From 2fad288b7aabd34fe071528cb6380c70d717fc67 Mon Sep 17 00:00:00 2001 From: PeterJFB Date: Tue, 9 May 2023 19:05:19 +0200 Subject: [PATCH] Implement qr-scanner, /qr and /activate_user in svelte --- app/routes/index.ts | 11 +- app/types/types.ts | 5 +- package.json | 7 +- src/lib/assets/ding.mp3 | Bin 0 -> 23956 bytes src/lib/assets/error.mp3 | Bin 0 -> 11241 bytes src/lib/components/Navbar.svelte | 16 +- src/lib/stores.ts | 8 +- src/lib/utils/cardKeyScanStore.ts | 212 ++++++++++++++++++ src/lib/utils/userApi.ts | 36 +++ src/routes/+layout.svelte | 4 +- .../moderator/(useCardKey)/+layout.svelte | 11 + .../(useCardKey)/activate_user/+page.svelte | 59 +++++ .../moderator/(useCardKey)/qr/+page.svelte | 82 +++++++ .../(useCardKey)/showqr/+page.svelte | 64 ++++++ .../moderator/activate_user/+page.svelte | 0 src/routes/moderator/qr/+page.svelte | 0 .../moderator/serial_error/+page.svelte | 43 ++++ tsconfig.json | 2 +- types.d.ts | 4 + yarn.lock | 62 +++-- 20 files changed, 587 insertions(+), 39 deletions(-) create mode 100644 src/lib/assets/ding.mp3 create mode 100644 src/lib/assets/error.mp3 create mode 100644 src/lib/utils/cardKeyScanStore.ts create mode 100644 src/lib/utils/userApi.ts create mode 100644 src/routes/moderator/(useCardKey)/+layout.svelte create mode 100644 src/routes/moderator/(useCardKey)/activate_user/+page.svelte create mode 100644 src/routes/moderator/(useCardKey)/qr/+page.svelte create mode 100644 src/routes/moderator/(useCardKey)/showqr/+page.svelte delete mode 100644 src/routes/moderator/activate_user/+page.svelte delete mode 100644 src/routes/moderator/qr/+page.svelte create mode 100644 src/routes/moderator/serial_error/+page.svelte diff --git a/app/routes/index.ts b/app/routes/index.ts index 46c2e1f8..9153c0aa 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -9,7 +9,7 @@ import { checkModeratorPartial, } from './helpers'; import env from '../../env'; -import { URLSearchParams } from 'url'; +import QueryString from 'qs'; let usage = { test: '2023-01-02' }; if (['production', 'development'].includes(process.env.NODE_ENV)) { import('../../usage.yml').then((usage_yml) => (usage = usage_yml.default)); @@ -74,9 +74,12 @@ router.get('/healthz', (req, res) => { router.get('*', (req, res, next) => { if (env.NODE_ENV === 'development') { // Prevent proxy recursion - const p = new URLSearchParams(req.params); - p.append('devproxy', '1'); - return res.redirect(env.FRONTEND_URL + req.path + '?' + p.toString()); + (req.query as any).devproxy = true; + return res.redirect( + env.FRONTEND_URL + + req.path + + QueryString.stringify(req.query, { addQueryPrefix: true }) + ); } next(); }); diff --git a/app/types/types.ts b/app/types/types.ts index 4481aa15..a4a1f0d9 100644 --- a/app/types/types.ts +++ b/app/types/types.ts @@ -79,8 +79,7 @@ export interface IUserMethods { } export type UserType = IUser; -export interface UserModel - extends Model, IUserMethods> { +export type UserModel = Model, IUserMethods> & { // statics authenticate( username: string, @@ -93,7 +92,7 @@ export interface UserModel body: IUser, password: string ): Promise>; -} +}; interface IVote { _id: string; diff --git a/package.json b/package.json index 8a37b0a7..61fcabbd 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "license": "MIT", "dependencies": { + "@types/dom-serial": "1.0.3", "@types/node": "18.15.3", "@types/qrcode": "1.5.0", "@types/sortablejs": "1.15.1", @@ -51,7 +52,7 @@ "connect-mongo": "4.6.0", "cookie-parser": "1.4.6", "crypto-browserify": "3.12.0", - "crypto-random-string": "1.0.0", + "crypto-random-string": "5.0.0", "csrf-sync": "4.0.1", "css-loader": "6.3.0", "express": "4.18.2", @@ -61,7 +62,7 @@ "ioredis": "5.2.4", "lodash": "4.17.21", "method-override": "3.0.0", - "mongoose": "6.8.3", + "mongoose": "6.11.5", "nib": "1.1.2", "nodemailer": "6.8.0", "nyc": "15.1.0", @@ -72,6 +73,7 @@ "promptly": "2.1.0", "qr-scanner": "1.4.2", "qrcode": "1.3.4", + "qs": "6.11.2", "raven": "2.6.4", "redlock": "5.0.0-beta.2", "serve-favicon": "2.5.0", @@ -107,6 +109,7 @@ "@types/raven": "2.5.4", "@types/serve-favicon": "2.5.3", "@types/socket.io": "3.0.2", + "@types/w3c-web-nfc": "1.0.0", "@types/webpack": "5.28.0", "@types/yaml": "1.9.7", "@typescript-eslint/eslint-plugin": "5.50.0", diff --git a/src/lib/assets/ding.mp3 b/src/lib/assets/ding.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c148fd47a04532dd428ec84b80dbe3e2c7006e41 GIT binary patch literal 23956 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 WkSI93aE*)V|Knx&fA4=^1^x%*lB!Su literal 0 HcmV?d00001 diff --git a/src/lib/assets/error.mp3 b/src/lib/assets/error.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4b7bed54e67fe4d2614073bf3c687ed4ad1a75dd GIT binary patch literal 11241 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
  • - Registrer bruker + Registrer bruker
  • - Generer bruker + Generer bruker
  • - QR + QR
  • - Aktiver bruker + Aktiver bruker
  • - Mistet kort + Mistet kort
  • - Register + Register
  • - Deaktiver brukere + + Deaktiver brukere +
  • {:else if !$page.url.pathname.includes('login')} diff --git a/src/lib/stores.ts b/src/lib/stores.ts index ef552fa7..100430af 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -1,10 +1,11 @@ import short from 'short-uuid'; -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; const xsrf = writable(''); const createAlerts = () => { - const { subscribe, set, update } = writable([]); + const alertList = writable([]); + const { subscribe, set, update } = alertList; const FADE_DURATION = 500; const CLOSE_DELAY = 5000; @@ -38,6 +39,9 @@ const createAlerts = () => { removeAll: () => { set([]); }, + getLastAlert: () => { + return get(alertList).slice().pop(); + }, FADE_DURATION, CLOSE_DELAY, }; diff --git a/src/lib/utils/cardKeyScanStore.ts b/src/lib/utils/cardKeyScanStore.ts new file mode 100644 index 00000000..c248ca0f --- /dev/null +++ b/src/lib/utils/cardKeyScanStore.ts @@ -0,0 +1,212 @@ +import { alerts } from '$lib/stores'; +import { onMount } from 'svelte'; +import { get, writable } from 'svelte/store'; + +// Stateless helper functions +const checksum = (data: number[]) => + data.reduce((previousValue, currentValue) => previousValue ^ currentValue); + +const createMessage = (command: number, data: number[]) => { + const payload = [data.length + 1, command, ...data]; + payload.push(checksum(payload)); + + return new Uint8Array([0xaa, 0x00, ...payload, 0xbb]).buffer; +}; + +const convertUID = (data: string[]) => { + const reversed = data + .join('') + .match(/.{1,2}/g) + .reverse() + .join(''); + return parseInt(reversed, 16); +}; + +const validate = (data: string[], receivedChecksum: string) => { + const dataDecimal = data.map((item) => parseInt(item, 16)); + const calculatedChecksum = checksum(dataDecimal); + return Math.abs(calculatedChecksum % 255) === parseInt(receivedChecksum, 16); +}; + +// prettier-ignore +const replies = { + '00': 'OK', + '01': 'ERROR', + '83': 'NO CARD', + '87': 'UNKNOWN INTERNAL ERROR', + '85': 'UNKNOWN COMMAND', + '84': 'RESPONSE ERROR', + '82': 'READER TIMEOUT', + '90': 'CARD DOES NOT SUPPORT THIS COMMAND', + '8f': 'UNSUPPORTED CARD IN NFC WRITE MODE', +}; + +const readCardCommand = createMessage(0x25, [0x26, 0x00]); + +const parseData = (response: number[]) => { + const hexValues = []; + for (let i = 0; i < response.length; i += 1) { + hexValues.push((response[i] < 16 ? '0' : '') + response[i].toString(16)); + } + const stationId = hexValues[1]; + const length = hexValues[2]; + const status: keyof typeof replies = hexValues[3] as keyof typeof replies; + const flag = hexValues[4]; + const data = hexValues.slice(5, hexValues.length - 1); + const checksum = hexValues[hexValues.length - 1]; + const valid = validate([stationId, length, status, flag, ...data], checksum); + + const statusReply = replies[status]; + return { + valid: valid, + data: valid && statusReply === 'OK' ? convertUID(data) : data, + status: statusReply, + }; +}; + +const DUMMY_READER_TEXT = `VOTE DUMMY READER MODE + +You are now in dummy reader mode of VOTE. Use the global function "scanCard" to scan a card. The function takes the card UID as the first (and only) parameter, and the UID can be both a string or a number. + +Usage: scanCard(123) // where 123 is the cardId `; + +// Writable svelte store: https://svelte.dev/docs#run-time-svelte-store-writable +export const cardKeyScanStore = writable<{ cardKey: number; time: number }>( + { cardKey: null, time: null }, + (set) => { + // Called whenever number of subscribers goes from zero to one + + let ndef: NDEFReader = null; + let serialDevice: { + writer: WritableStreamDefaultWriter; + reader: ReadableStreamDefaultReader; + } = null; + const readerBusy = writable(false); + const serialTimeout = writable(null); + + // The scanner depends on values from window + onMount(async () => { + // Check first if dummyReader was requirested + if (window.location.href.includes('dummyReader')) { + window.scanCard = (cardKey: number) => + set({ cardKey, time: Date.now() }); + console.error(DUMMY_READER_TEXT); + } else { + // Attempt to open a connection + try { + if ( + window.navigator.userAgent.includes('Android') && + 'NDEFReader' in window && + (!window.navigator.serial || + window.confirm( + 'You are using an Android device that (might) support web nfc. Click OK to use web nfc, and cancel to fallback to using a usb serial device.' + )) + ) { + const ndefReader = new NDEFReader(); + await ndefReader.scan(); + ndef = ndefReader; + } else { + const port = await window.navigator.serial.requestPort(); + await port.open({ baudRate: 9600 }); + serialDevice = { + writer: port.writable.getWriter(), + reader: port.readable.getReader(), + }; + } + } catch (e) { + if (window.navigator.userAgent.includes('Android')) { + alerts.push(e, 'ERROR'); + } + window.location.assign('/moderator/serial_error'); + console.error(e); + } + } + + // Poll open connections (if one was created) + if (ndef) { + ndef.onreading = ({ message, serialNumber }) => { + const data = convertUID(serialNumber.split(':')); + set({ cardKey: data, time: Date.now() }); + }; + } else if (serialDevice && !get(serialTimeout)) { + // Stateful helper functions for serial device + const onComplete = (input: number[]) => { + const { valid, status, data } = parseData(input); + if (valid && status == 'OK' && typeof data === 'number') { + // Debounce + if ( + data !== get(cardKeyScanStore).cardKey || + Date.now() - get(cardKeyScanStore).time > 2000 + ) { + // data = card key + set({ cardKey: data, time: Date.now() }); + } + } + }; + const readResult = async () => { + const message = []; + let finished = false; + let isReaderBusy = true; + // Keep reading bytes until the "end" byte is sent + // The "end" byte is 0xbb + while (!finished) { + // Stop the read if the device is busy somewhere else + isReaderBusy = true; + readerBusy.update((readerBusy) => { + isReaderBusy = readerBusy; + return true; + }); + if (isReaderBusy) break; + const { value } = await serialDevice.reader.read(); + readerBusy.set(false); + for (let i = 0; i < value.length; i++) { + // First byte in a message should be 170, otherwise ignore and keep on going + if (message.length === 0 && value[i] !== 170) { + continue; + } + // Second byte in a message should be 255, otherwise discard and keep on going + if (message.length === 1 && value[i] !== 255) { + // If value is 170, treat it as the first value, and keep on. Otherwise discard + if (value[i] !== 170) { + message.length = 0; + } + continue; + } + + if (message.length > 3 && message.length >= message[2] + 4) { + finished = true; + break; + } + message.push(value[i]); + } + } + onComplete(message); + }; + + // Constantly send the readCardCommand and read the result. + // If there is no card, the result will be an error status, + // which is handled in the onComplete function + const runPoll = async () => { + try { + serialDevice.writer.write(readCardCommand); + await readResult(); + } catch (e) { + console.error('Error doing card stuff', e); + readerBusy.set(false); + } finally { + serialTimeout.set(setTimeout(runPoll, 150)); + } + }; + runPoll(); + } + }); + + return () => { + // Called when number of subscribers goes to zero + serialTimeout.update((timeout) => { + clearTimeout(timeout); + return null; + }); + }; + } +); diff --git a/src/lib/utils/userApi.ts b/src/lib/utils/userApi.ts new file mode 100644 index 00000000..2253d4f9 --- /dev/null +++ b/src/lib/utils/userApi.ts @@ -0,0 +1,36 @@ +import callApi from './callApi'; + +export const toggleUser = (cardKey: number | string) => { + return callApi('/user/' + cardKey + '/toggle_active', 'POST'); +}; + +export const createUser = (user: Record) => { + return callApi('/user', 'POST', user); +}; + +export const generateUser = (user: Record) => { + return callApi('/user/generate', 'POST', user); +}; + +export const changeCard = (user: Record) => { + return callApi('/user/' + user.username + '/change_card', 'PUT', user); +}; + +export const countActiveUsers = () => { + return callApi('/user/count?active=true'); +}; + +export const deactivateNonAdminUsers = () => { + return callApi('/user/deactivate', 'POST'); +}; + +const userApi = { + toggleUser, + createUser, + generateUser, + changeCard, + countActiveUsers, + deactivateNonAdminUsers, +}; + +export default userApi; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 89b0e88a..64646abd 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -17,7 +17,9 @@ - + + +