---
title: The Authentication of Survey
tags: v1
---
# The Authentication of Survey
[TOC]
# 目標
- 經由老師認證後,才讓填寫完表單上鏈
# UX
1. Student complete form

2. teacher get action required to authenticate

3. teacher click button authenticate to review the content

4. click authenticate or reject

5. Student can see the survey is authenticaed/unauthenticated/rejected three kind of status in Resume

6. Student can resent the survey if status is Unauthenticated or rejected

# 參考
## 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

- 圖2

- 圖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 想法

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`