---
# System prepended metadata

title: The Authentication of Survey
tags: [v1]

---

---
title: The Authentication of Survey
tags: v1
---

# The Authentication of Survey
[TOC]

# 目標

- 經由老師認證後，才讓填寫完表單上鏈
 

# UX
1. Student complete form
![](https://i.imgur.com/ZbE7taq.png)

2. teacher get action required to authenticate
![](https://i.imgur.com/NWLKd3G.png)

3. teacher click button authenticate to review the content
![](https://i.imgur.com/xO17Jsy.png)

4. click authenticate or reject
![](https://i.imgur.com/adJoqks.png)

5. Student can see the survey is authenticaed/unauthenticated/rejected three kind of status in Resume
![](https://i.imgur.com/JY5i8Zt.png)



6. Student can resent the survey if status is  Unauthenticated or rejected
![](https://i.imgur.com/X2lCjfM.png)


 

# 參考

## AuthSec
```
fio[48].code

async (contract) => {
  // 準備資料
  var obj = contract.obj;
  var fio = contract.fio;
  var input = fio.input;
  var output = fio.output;
  var msg = obj.dict(input

  // AuthSec確認
  var confirm = await contract.AuthSec_confirm('fio:' + msg.Name, 'FiO打卡確認', '您是否確認要將此筆打卡資料上鏈？', msg);
  if (!confirm) throw '拒絕上鏈';

  // 將JSON字串化後簽章
  var signed = await contract.GPG_sign(JSON.stringify(msg));

  // 發佈到IOTA
  var bundle = await contract.IOTA_send(signed);

  // 記錄資料
  var date = new Date();
  var hash = bundle.map(x => x.hash).join('+');

  // 檢查是否有token_id欄位
  if (output.token_id && obj.has(output.token_id)) {
    // 完整歷史資料
    var token_id = parseInt(obj.get(output.token_id));
    if (!token_id) {
      // 新增
      token_id = await contract.db.add('iota', {date, hash, msg});
      await obj.set(output.token_id, token_id);
    }
    else {
      // 更新
      await contract.db.set('iota', token_id, {date, hash, msg});
    }
    await contract.db.add('iota_log', {date, token_id, hash, msg});
  }

  // 更新Google Sheet
  await obj.set(output.hash, hash);
};
```


## 規劃 1
[圖1](https://gitlab.com/fio.io/tms/-/blob/dev/sheet#L131)
[圖2](https://gitlab.com/fio.io/tms/-/blob/dev/sheet#L120-122)
- 圖1
![](https://i.imgur.com/ed4OZyK.png)
- 圖2
![](https://i.imgur.com/lkvt8Nr.png)

- 圖2標示處 如果是 Done 、Error 或 Pending 則會跳過，學生填完表單後，將此欄位都改成 Pending，老師認證後，改成其他參數 => 執行 S.M.A.R.T Contract

## S.M.A.R.T Contract
```
  const code_IOTA = `async (contract) => {
  // initiate data object
  var obj = contract.obj;
  var fio = contract.fio;
  var msg = obj.dict(fio.input);
  
  // stringify JSON object and sign with GPG
  var signed = await contract.GPG_sign(JSON.stringify(msg));

  // publish cipher to IOTA
  var bundle = await contract.IOTA_send(signed);
  var hash = bundle.map(x => x.hash).join('+');
  await contract.db.add('iota_log', {date: new Date(), obj_id: obj.obj_id, hash, msg});

  // update Google Sheet
  await obj.set(fio.output.Hash, hash);
}`;
```
## 規劃 2
```
  const code_IOTA = `async (contract) => {
  // initiate data object
  var obj = contract.obj;
  var fio = contract.fio;
  var msg = obj.dict(fio.input);
  
  新增下方程式碼 LearningCurve

  // stringify JSON object and sign with GPG
  var signed = await contract.GPG_sign(JSON.stringify(msg));

  // publish cipher to IOTA
  var bundle = await contract.IOTA_send(signed);
  var hash = bundle.map(x => x.hash).join('+');
  await contract.db.add('iota_log', {date: new Date(), obj_id: obj.obj_id, hash, msg});

  // update Google Sheet
  await obj.set(fio.output.Hash, hash);
}`;
```

Jack 想法
![](https://i.imgur.com/4qMZJuD.jpg)
1. 最簡單的做法就是在存入 db 時，加一個欄位去判斷是否有被驗證

2. 寫進 S.M.A.R.T Contract 


# 實作

## Learning Curve S.M.A.R.T. Contract

- feature: support authentication for solution: `Learning Curve`
- feature: support contract.api，能在 S.M.A.R.T. Contract 建客製化的 API
    - support contract.api.get
    - support contract.api.post
    - support contract.api.put
    - support contract.api.delete
    - support contract.api.patch

### S.M.A.R.T. Contract
``` javascript=
const code_LearningCurve = `async (contract) => {
```

``` javascript=+
  // initiate data object
  var obj = contract.obj;
  var fio = contract.fio;
  var msg = obj.dict(fio.input);
  var date = new Date();

  // load authenication status
  /* auth schema: {
   *   obj_id: obj.obj_id,  // the row number in Google Sheet
   *   status,              // see bellow table
   *   date,                // ISO8601
   * }
  **/
  /* (tips: 請使用定寬字元)
   *| no  |  S.M.A.R.T. Contract | scenario                 | write S.M.A.R.T. Contract | On Chain | rsAuth.status  | front_end_showing_log |
   *| --- | -------------------- | ------------------------ | ------------------------- | -------- | -------------- | --------------------- |
   *| x   |  obj init            | 執行 S.M.A.R.T. Contract | no                        | On Chain | undefined      |                       |
   *| 1   |  obj.run()           | Student 輸入完資料       | yes                       | pending  | data-input     |                       |
   *| 2   |  obj.run()           | 等老師認證               | yes                       | pending  | awaiting-auth  | v                     |
   *| 3   |  obj.run()           | 老師已認證，同意         | yes                       | pending  | accepted       |                       |
   *| 4   |  obj.run()           | 老師已認證，不同意       | yes                       | pending  | rejected       | v                     |
   *| 5   |  obj.run()           | 上鏈中                   | yes                       | pending  | do-on-chain    |                       |
   *| 6   |  obj.done()          | 上鏈完成：成功           | no                        | Done     | do-on-chain    | v                     |
   *| 7   |  obj.error()         | 上鏈完成：失敗           | no                        | Error    | do-on-chain    |                       |
  **/
  var rsAuth = await contract.db.one('auth', {obj_id: obj.obj_id});

  // awaiting accept or reject
  if (!rsAuth) {
    rsAuth = {obj_id: obj.obj_id, status: undefined, date};

    // 1: student fill-in the form
    rsAuth.status = 'data-input';
    let _id = await contract.db.add('auth', rsAuth);

    // 2: awaiting-auth
    rsAuth.status = 'awaiting-auth';
    await contract.db.set('auth', _id, rsAuth);
    await contract.db.add('front_end_showing_log', rsAuth);

    // sent to instruct the operating system to halt a process.
    return contract.signal('SIGSTOP');
  }
  if (!['accepted', 'rejected'].includes(rsAuth.status)) return;

  // already accepted or rejected
  rsAuth.date = date;
  if (rsAuth.status === 'accepted') {
    // 3: accepted
    rsAuth.status = 'do-on-chain';
    await contract.db.set('auth', rsAuth._id, rsAuth);

    // stringify JSON object and sign with GPG
    var signed = await contract.GPG_sign(JSON.stringify(msg));

    // publish cipher to IOTA
    var bundle = await contract.IOTA_send(signed);
    var hash = bundle.map(x => x.hash).join('+');
    var iota_log = {date: new Date(), obj_id: obj.obj_id, hash, msg};
    await contract.db.add('iota_log', iota_log);
    await contract.db.add('front_end_showing_log', {... rsAuth, ... iota_log});

    // update Google Sheet
    await obj.set(fio.output.Hash, hash);
  } else if (rsAuth.status === 'rejected') {
    // 4: rejected: log
    rsAuth.status = 'rejected';
    await contract.db.set('auth', rsAuth._id, rsAuth);
    await contract.db.add('front_end_showing_log', rsAuth);
  }

  // sent to instruct the operating system to continue a paused process.
  return contract.signal('SIGCONT');
```
``` javascript=+
};`;
```


### API

#### POST /fio/:fio_id/api/status/:authenticated

- query string
    - fio_id: number, fio app id
    - authenticated: string of ["accepted", rejected"]

``` javascript=
//contract.api.post('/api/status/authenticated', (req, res) => { // feature
api.post('/:fio_id/api/status/:authenticated', async (req, res) => {
  const fio_id = req.params.fio_id;
  const obj_id = req.body.obj_id;
  const authenticated = req.params.authenticated;
  const AUTHENTICATED_LIST = ['accepted', 'rejected'];
  const COLLECTION_PREFIX = `fio_${fio_id}_`;
  if (!AUTHENTICATED_LIST.includes(authenticated))
    return res.err(1, "Not allowed param authenticated: '${req.params.authenticated}'");
  if (!obj_id) return res.err(2, "The param obj_id is undefined.");
  if (!obj_id) return res.err(6, "The param obj_id must be string.");

  // get fio obj
  let fio = await Mongo.get('fio', fio_id);
  if (!fio) return res.err(4, 'Invalid fio!');

  // check user permission: only owner, approver can use this API
  // permission: [v] 'admin', [v] 'owner', [v] 'approver', [x] others including 'user'
  if (req.session.type !== 'admin' // admin
    || req.session.username !== fio.username // owner
  ) {
    let approvers = fio.invitations.filter(inv => inv.status === 'active' && inv.type === 'approver');
    if (!fio.invitations
      || !fio.invitations.length
      || !approvers.includes(req.session.username) // approver
      ) return res.err(5, 'You have no permission.');
  }

  // SELECT * FROM `fio_${fio._id}_obj` WHERE _id = obj_id
  let rsObj = await Mongo.get(`${COLLECTION_PREFIX}obj`, obj_id);
  if (!rsObj) return res.err(3, "There is no data for obj_id: '${obj_id}'");

  // SELECT * FROM `fio_${fio._id}_auth` WHERE _id = obj_id
  let rsAuth = await Mongo.one(`${COLLECTION_PREFIX}auth`, {obj_id});

  // set status = :authenticated
  rsAuth.status = authenticated;
  rsAuth.date = new Date();
  console.log(`${COLLECTION_PREFIX}auth`, {_id: rsAuth._id, rsAuth});
  let rs = await Mongo.set(`${COLLECTION_PREFIX}auth`, rsAuth._id, rsAuth);

  // update sheet
  await SheetHelper.setValueById(fio_id, obj_id, fio.confirm, 'v');

  return res.ok({fio_id: fio._id, ... rsAuth, rs});
});
```

### (deprecated) Write APIs in S.M.A.R.T. Contract
``` javascript=

  //*** Custom APIs ***/
  let do_on_chain = () => {
    // stringify JSON object and sign with GPG
    var signed = await contract.GPG_sign(JSON.stringify(msg));
  
    // publish cipher to IOTA
    var bundle = await contract.IOTA_send(signed);
    var hash = bundle.map(x => x.hash).join('+');
    await contract.db.add('iota_log', {date: new Date(), obj_id: obj.obj_id, hash, msg});
  
    // update Google Sheet
    await obj.set(fio.output.Hash, hash);
  }
  
  // new api: set rsAuth.status = 'authenticated'
  // api.post('/:fio_id/api/status/authenticated', (req, res) => {})
  contract.api.post('/api/status/authenticated', (req, res) => {
    const AUTH_STATUS = 'authenticated';
    const DO_ONCHAIN = true;
  
    let rsAuth = await contract.db.get('auth', obj_id);
    if (!rsAuth) rsAuth = {obj_id, status, date};
    if (rsAuth.status === AUTH_STATUS) return rsAuth;
    rsAuth.status = AUTH_STATUS;
    rsAuth.date = new Date();
    await contract.db.set('auth', rsAuth);
    await contract.db.add('front_end_showing_log', rsAuth);
    
    if (DO_ONCHAIN) do_on_chain();
    res.ok({fio_id: fio._id,...auth});
  }
  
  
  // new api: set auth.status = 'rejected'
  // api.post('/:fio_id/api/status/authenticated', (req, res) => {})
  contract.api.post('/api/status/rejected', (req, res) => {
    const AUTH_STATUS = 'rejected';
    const DO_ONCHAIN = false;
  
    let rsAuth = await contract.db.get('auth', obj_id);
    if (!rsAuth) rsAuth = {obj_id, status, date};
    if (rsAuth.status === AUTH_STATUS) return auth;
    rsAuth.status = AUTH_STATUS;
    rsAuth.date = new Date();
    await contract.db.set('auth', rsAuth);
    await contract.db.add('front_end_showing_log', rsAuth);
    
    if (DO_ONCHAIN) do_on_chain();
    res.ok({fio_id: fio._id,...rsAuth});
  }
  
  // new api: get auth.status
  // api.get('/:fio_id/api/status', (req, res) => {})
  contract.api.get('/api/status', (req, res) => {
    let auth = await contract.db.get('auth', obj_id);
    return res.ok(auth);
  }
```


auth.status 說明

- pending 觸發條件：
    - 在 check sheet 有新資料時 authentication = pending
- authenticated 觸發條件：
    - User call api: `/:fio_id/api/status/authenticated`
- rejected 觸發條件：
    - User call api: `/:fio_id/api/status/rejected`
