--- 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`