diff --git a/packages/backend/index.ts b/packages/backend/index.ts index 4f01033e..8fa1e772 100644 --- a/packages/backend/index.ts +++ b/packages/backend/index.ts @@ -17,7 +17,7 @@ connect(config.mongo.host, { useNewUrlParser: true, useUnifiedTopology: true, us // No admin has been found -> Create a registration token and print it into the logs new RegistrationToken({ userIsDeletable: false }).save().then(token => { loggerFile.info('No administrator found!'); - loggerFile.info(`Visit ${config.rp.origin}/#/registration and use the following token: ${token.key}`); + loggerFile.info(`Visit ${config.rp.origin}/#/registration?token=${token.key} to register your user`); loggerFile.info(`This token is valid for 15 minutes.`); }); }); diff --git a/packages/backend/src/controllers/administrator/administrator.schema.ts b/packages/backend/src/controllers/administrator/administrator.schema.ts index 4fd331d5..b7c77d7c 100644 --- a/packages/backend/src/controllers/administrator/administrator.schema.ts +++ b/packages/backend/src/controllers/administrator/administrator.schema.ts @@ -1,6 +1,7 @@ /* tslint:disable:ban-types */ import { Schema, model, Model, Document } from 'mongoose'; -import { IAuthenticatorDocument } from '../authentication/authenticator.schema'; +import { IAuthenticatorDocument, authenticatorSchema } from './authenticator.schema'; +import { string } from '@hapi/joi'; export interface IAdministratorDocument extends Document { [_id: string]: any; @@ -16,7 +17,9 @@ const administratorSchema = new Schema({ createdAt: { type: Date, default: Date.now }, deletable: { type: Boolean, default: true }, currentChallenge: { type: String, default: null }, - device: { type: Schema.Types.ObjectId, ref: 'Authenticator' }, + device: { type: authenticatorSchema, default: null }, }); export const Administrator: Model = model('Administrator', administratorSchema); + +export const administratorUsernameValidationSchema = string().alphanum().required().min(8).max(64).description('Username of administrator'); diff --git a/packages/backend/src/controllers/authentication/authenticator.schema.ts b/packages/backend/src/controllers/administrator/authenticator.schema.ts similarity index 64% rename from packages/backend/src/controllers/authentication/authenticator.schema.ts rename to packages/backend/src/controllers/administrator/authenticator.schema.ts index 6b244e5b..efdf56ee 100644 --- a/packages/backend/src/controllers/authentication/authenticator.schema.ts +++ b/packages/backend/src/controllers/administrator/authenticator.schema.ts @@ -6,16 +6,12 @@ export interface IAuthenticatorDocument extends Document { credentialID: Buffer; credentialPublicKey: Buffer; counter: number; - // ['usb' | 'ble' | 'nfc' | 'internal'] transports?: AuthenticatorTransport[]; } -const authenticatorSchema = new Schema({ +export const authenticatorSchema = new Schema({ credentialID: { type: Buffer, required: true }, credentialPublicKey: { type: Buffer, required: true }, counter: { type: Number, required: true }, - // ['usb' | 'ble' | 'nfc' | 'internal'] transports: Array, }); - -export const Authenticator: Model = model('Authenticator', authenticatorSchema); diff --git a/packages/backend/src/controllers/administrator/index.ts b/packages/backend/src/controllers/administrator/index.ts index 508de310..21936ec2 100644 --- a/packages/backend/src/controllers/administrator/index.ts +++ b/packages/backend/src/controllers/administrator/index.ts @@ -1,52 +1,46 @@ import { Request, Response, NextFunction } from 'express'; -import { object, string } from '@hapi/joi'; import { loggerFile } from '../../configuration/logger'; -import { Administrator } from './administrator.schema'; +import { Administrator, administratorUsernameValidationSchema } from './administrator.schema'; import { ApiError, ApiSuccess } from '../../utils/api'; +import { RegistrationToken } from '../authentication/registrationToken.schema'; +import { config } from '../../configuration/environment'; +import { object } from '@hapi/joi'; -const addAdministratorRequestSchema = object({ - username: string().required().regex(/^[\w\s]+#\d{4}$/), - userid: string().required().regex(/^\d{18}$/), -}); +/**************************************** + * User input validation * + * **************************************/ +const adminAdministratorDeleteRequestSchema = object({ + username: administratorUsernameValidationSchema, +}).unknown(); -const deleteAdministratorRequestSchema = string().required().regex(/^\d{18}$/); + +/**************************************** + * Endpoint Handlers * + * **************************************/ /** - * Handles POST /api/settings/administrator requests - * and creates a new Administrator if possible + * POST /settings/administrator + * Creates and returns a new registration token + * * @param req Request * @param res Response * @param next NextFunction */ -export async function addAdministratorRequest(req: Request, res: Response, next: NextFunction) { +export async function adminAdministratorPostRequest(req: Request, res: Response, next: NextFunction) { try { - const administratorRequest = addAdministratorRequestSchema.validate(req.body); - if (administratorRequest.error) throw new ApiError(400, administratorRequest.error.message); - - // check if administrator exists at the database - const administrator = await Administrator.findOne({ $or: [ - { userName: administratorRequest.value.username }, - { userId: administratorRequest.value.userid } - ] }); - - // throw error, if admin name or id is already being used - if (administrator) { - if (administratorRequest.value.username === administrator.userName) - throw new ApiError(400, `Administrator ${administrator.userName} already exists`); - if (administratorRequest.value.userid === administrator.userId) - throw new ApiError(400, `Administrator with ID ${administrator.userId} already exists`); - } - // create new administrator - const adminObj = { - userId: administratorRequest.value.userid, - userName: administratorRequest.value.username, - }; + // 1. Create a new registration token + const registrationToken = await new RegistrationToken({ userIsDeletable: true }).save(); + if (registrationToken === null && registrationToken === undefined) throw new ApiError(500, 'Unable to create registration token'); - await new Administrator(adminObj).save(); + const responseBody = { + token: registrationToken.key, + origin: config.rp.origin, + lifetime: config.registrationTokenLifetime, + }; - const response = new ApiSuccess(201); + const response = new ApiSuccess(201, responseBody); next(response); } catch (err) { @@ -56,26 +50,27 @@ export async function addAdministratorRequest(req: Request, res: Response, next: } /** - * Handles GET /api/settings/administrator requests - * and responds with a list of all administrators. + * GET /api/settings/administrator + * + * Responds a list of all administrators. * @param req Request * @param res Response * @param next NextFunction */ -export async function getAdministratorListRequest(req: Request, res: Response, next: NextFunction) { +export async function adminAdministratorGetRequest(req: Request, res: Response, next: NextFunction) { try { - // Get administrators from database + // 1. Get administrators from database const administrators = await Administrator.find({}); - if (!administrators) throw new ApiError(503, 'Internal error while retrieving administrators'); + if (!administrators) throw new ApiError(500, 'Internal error while retrieving administrators'); - // Extract relevant details + // 2. Extract relevant details const administratorList = administrators.map(model => { return { - userName: model.get('userName'), - userId: model.get('userId'), + username: model.get('username'), createdAt: new Date(model.get('createdAt')).getTime(), deletable: model.get('deletable'), + hasDevice: !!model.get('device'), }; }); @@ -89,37 +84,41 @@ export async function getAdministratorListRequest(req: Request, res: Response, n } /** - * Handles DELETE /api/settings/administrator requests - * and deletes an administrator specified by user id from the database + * DELETE /settings/administrator/{username} + * + * Deletes an administrator specified by user id from the database * @param req Request * @param res Response * @param next NextFunction */ -export async function deleteAdministratorRequest(req: Request, res: Response, next: NextFunction) { +export async function adminAdministratorDeleteRequest(req: Request, res: Response, next: NextFunction) { try { - const administratorRequest = deleteAdministratorRequestSchema.validate(req.params.id); - if (administratorRequest.error) throw new ApiError(400, administratorRequest.error.message); - // Delete administrator from database - const administrator = await Administrator.findOne({ - userId: administratorRequest.value - }); + // 1. Validate user input + const administratorDeleteRequest = adminAdministratorDeleteRequestSchema.validate(req.params); + if (administratorDeleteRequest.error) throw new ApiError(400, administratorDeleteRequest.error.message); - // Throw error, if admin user id is not in database - if (!administrator) { - throw new ApiError(404, `Administrator with id ${administratorRequest.value} not found in database`); + // 2. Get requested admin from database + const administrator = await Administrator.findOne({ username: administratorDeleteRequest.value.username }); + if (administrator === null || administrator === undefined) { + throw new ApiError(404, `Administrator ${administratorDeleteRequest.value.username} not found`); } - // Throw error, if admin is not deletable - if (!administrator.deletable) { - throw new ApiError(403, `Administrator with id ${administratorRequest.value} is not deletable`); - } + // 3. Validate that admin is deletable and throw error if not + if (!administrator.deletable) throw new ApiError(403, `Administrator ${administratorDeleteRequest.value.username} is not deletable`); - administrator.deleteOne(); + try { + // 4. Delete admin + await administrator.deleteOne(); + + const response = new ApiSuccess(204); + next(response); + } catch (error) { + loggerFile.error(error.message); + throw new ApiError(500, 'Failed to delete administrator or authenticator'); + } - const response = new ApiSuccess(204); - next(response); } catch (err) { loggerFile.error(err.message); diff --git a/packages/backend/src/controllers/authentication/index.ts b/packages/backend/src/controllers/authentication/index.ts index 7d7fed0c..0e4afb28 100644 --- a/packages/backend/src/controllers/authentication/index.ts +++ b/packages/backend/src/controllers/authentication/index.ts @@ -3,43 +3,37 @@ */ import { Request, Response, NextFunction } from 'express'; -import { object, string, CustomHelpers } from '@hapi/joi'; -import { Administrator, IAdministratorDocument } from '../administrator/administrator.schema'; +import { object } from '@hapi/joi'; +import { Administrator, administratorUsernameValidationSchema, IAdministratorDocument } from '../administrator/administrator.schema'; import { loggerFile } from '../../configuration/logger'; import jwt from 'jsonwebtoken'; import expressjwt from 'express-jwt'; import { config } from '../../configuration/environment'; import { ApiError, ApiSuccess } from '../../utils/api'; import { generateAssertionOptions, generateAttestationOptions, GenerateAttestationOptionsOpts, verifyAssertionResponse, verifyAttestationResponse } from '@simplewebauthn/server'; -import { validate as validateUUID } from 'uuid'; -import { RegistrationToken } from './registrationToken.schema'; -import { Authenticator, IAuthenticatorDocument } from './authenticator.schema'; +import { RegistrationToken, registrationTokenValidationSchema } from './registrationToken.schema'; +import { IAuthenticatorDocument } from '../administrator/authenticator.schema'; /**************************************** * User input validation * * **************************************/ -const isUUID = (value: any, helper: CustomHelpers) : any => { - if (!validateUUID(value)) return helper.error('any.invalid'); - return value; -}; - const authAssertionGetRequestSchema = object({ - username: string().alphanum().required().min(8).max(64).description('Username of admin to register'), + username: administratorUsernameValidationSchema, }); const authAssertionPostRequestSchema = object({ - username: string().alphanum().required().min(8).max(64).description('Username of admin to register'), + username: administratorUsernameValidationSchema, assertionResponse: object().unknown().required().description('Webauthn challenge') }); const authAttestationGetRequestSchema = object({ - username: string().alphanum().required().min(8).max(64).description('Username of admin to register'), - token: string().required().custom(isUUID).description('Registration token'), + username: administratorUsernameValidationSchema, + token: registrationTokenValidationSchema, }); const authAttestationPostRequestSchema = object({ - username: string().alphanum().required().min(8).max(64).description('Username of admin to register'), - token: string().required().custom(isUUID).description('Registration token'), + username: administratorUsernameValidationSchema, + token: registrationTokenValidationSchema, attestationResponse: object().unknown().required().description('Webauthn challenge') }); @@ -129,7 +123,7 @@ export async function authAttestationGetRequest(req: Request, res: Response, nex // 2. Validate registration token const registrationTokenDoc = await RegistrationToken.findOne({ 'key': attestationGetRequest.value.token }); - if (registrationTokenDoc === null) throw new ApiError(404, 'Registration token not found'); + if (registrationTokenDoc === null) throw new ApiError(404, 'Registration token not found or expired'); // 3. Get user document; create if it does not exist const userDoc = await Administrator.findOneAndUpdate({ username: attestationGetRequest.value.username }, {}, { upsert: true, new: true }); @@ -198,7 +192,7 @@ export async function authAttestationGetRequest(req: Request, res: Response, nex // 2. Validate registration token const registrationTokenDoc = await RegistrationToken.findOne({ 'key': attestationPostRequest.value.token }); - if (registrationTokenDoc === null) throw new ApiError(404, 'Registration token not found'); + if (registrationTokenDoc === null) throw new ApiError(404, 'Registration token not found or expired'); // 3. Get user document const userDoc = await Administrator.findOne({ username: attestationPostRequest.value.username }); @@ -220,13 +214,7 @@ export async function authAttestationGetRequest(req: Request, res: Response, nex // 5. Save authenticator const { credentialPublicKey, credentialID, counter } = attestationInfo; - const authenticator = await new Authenticator({ - credentialID, - credentialPublicKey, - counter - }).save(); - - userDoc.device = authenticator; + userDoc.device = { credentialID, credentialPublicKey, counter } as IAuthenticatorDocument; // 6. Save whether user is deletable or not userDoc.deletable = registrationTokenDoc.userIsDeletable; @@ -269,7 +257,7 @@ export async function authAttestationGetRequest(req: Request, res: Response, nex if (assertionGetRequest.error) throw new ApiError(400, assertionGetRequest.error.message); // 2. Get user document - const userDoc = await Administrator.findOne({ username: assertionGetRequest.value.username }).populate('device'); + const userDoc = await Administrator.findOne({ username: assertionGetRequest.value.username }); if (userDoc === null) throw new ApiError(404, 'User not found'); if (userDoc.device === null || userDoc.device === undefined) throw new ApiError(403, 'User not registered'); @@ -313,7 +301,7 @@ export async function authAttestationGetRequest(req: Request, res: Response, nex if (assertionPostRequest.error) throw new ApiError(400, assertionPostRequest.error.message); // 2. Get user document - const userDoc: IAdministratorDocument = await Administrator.findOne({ username: assertionPostRequest.value.username }).populate('device'); + const userDoc: IAdministratorDocument = await Administrator.findOne({ username: assertionPostRequest.value.username }); if (userDoc === null) throw new ApiError(404, 'User not found'); if (userDoc.device === null || userDoc.device === undefined) throw new ApiError(403, 'User not registered'); if (userDoc.currentChallenge === undefined || userDoc.currentChallenge === null) throw new ApiError(400, 'User has no pending challenge'); diff --git a/packages/backend/src/controllers/authentication/registrationToken.schema.ts b/packages/backend/src/controllers/authentication/registrationToken.schema.ts index e5e43f59..dde2d68c 100644 --- a/packages/backend/src/controllers/authentication/registrationToken.schema.ts +++ b/packages/backend/src/controllers/authentication/registrationToken.schema.ts @@ -1,7 +1,8 @@ /* tslint:disable:ban-types */ import { config } from '../../configuration/environment'; import { Schema, model, Model, Document } from 'mongoose'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4, validate as validateUUID } from 'uuid'; +import { string, CustomHelpers } from '@hapi/joi'; interface IRegistrationTokenDocument extends Document { [_id: string]: any; @@ -21,3 +22,10 @@ const registrationTokenSchema = new Schema({ }); export const RegistrationToken: Model = model('RegistrationToken', registrationTokenSchema); + +const isUUID = (value: any, helper: CustomHelpers) : any => { + if (!validateUUID(value)) return helper.error('any.invalid'); + return value; +}; + +export const registrationTokenValidationSchema = string().required().custom(isUUID).description('Registration token'); diff --git a/packages/backend/src/routes/settings.routes.ts b/packages/backend/src/routes/settings.routes.ts index 58304b51..8e020738 100644 --- a/packages/backend/src/routes/settings.routes.ts +++ b/packages/backend/src/routes/settings.routes.ts @@ -2,7 +2,7 @@ import { Router } from 'express'; import { setRefreshRateRequest, getRefreshRateRequest } from '../controllers/moodle/refreshRate'; import { getCourseListRequest, setCourseRequest } from '../controllers/courseList/courseList'; import { setDiscordChannelRequest, getDiscordChannelRequest } from '../controllers/discordChannel/discordChannel'; -// import { addAdministratorRequest, getAdministratorListRequest, deleteAdministratorRequest } from '../controllers/administrator'; +import { adminAdministratorPostRequest, adminAdministratorGetRequest,adminAdministratorDeleteRequest } from '../controllers/administrator'; import { getStatusRequest } from '../controllers/status/status'; export const settingsRoutes = Router(); @@ -13,7 +13,7 @@ settingsRoutes.get('/courses', getCourseListRequest); settingsRoutes.put('/courses/:id', setCourseRequest); settingsRoutes.get('/discordChannel', getDiscordChannelRequest); settingsRoutes.put('/discordChannel', setDiscordChannelRequest); -// settingsRoutes.post('/administrator', addAdministratorRequest); -// settingsRoutes.get('/administrator', getAdministratorListRequest); -// settingsRoutes.delete('/administrator/:id', deleteAdministratorRequest); +settingsRoutes.post('/administrators', adminAdministratorPostRequest); +settingsRoutes.get('/administrators', adminAdministratorGetRequest); +settingsRoutes.delete('/administrators/:username', adminAdministratorDeleteRequest); settingsRoutes.get('/status', getStatusRequest); diff --git a/packages/backend/tests/administrator.spec.off.ts b/packages/backend/tests/administrator.spec.off.ts deleted file mode 100644 index c2326a83..00000000 --- a/packages/backend/tests/administrator.spec.off.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { loggerFile } from "../src/configuration/logger"; -import { addAdministratorRequest, deleteAdministratorRequest, getAdministratorListRequest } from "../src/controllers/administrator"; -import mockingoose from 'mockingoose'; -import { Request, Response } from "express"; -import { Administrator } from "../src/controllers/administrator/administrator.schema"; -import { ApiError, ApiSuccess } from "../src/utils/api"; - -jest.mock('../src/configuration/environment.ts'); - -describe('administrator/index.ts addAdministratorRequest', () => { - - let mockRequest: Request; - let mockResponse: Response; - let mockUser: any; - let mockAdministrator: any; - let mockNext: jest.Mock; - let spyLogger: jest.SpyInstance; - - beforeEach(() => { - mockRequest = { - headers: {}, - body: {}, - } as Request; - - mockResponse = { - status: jest.fn(() => mockResponse), - json: jest.fn(), - send: jest.fn(), - end: jest.fn() - } as any as Response; - - mockUser = { - username: 'test#1231', - userid: '123456789123456789', - } - - mockAdministrator = {}; - - mockNext = jest.fn(); - spyLogger = jest.spyOn(loggerFile, 'error'); - - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - - it('should log error if wrong addAdministratorRequest provided', async () => { - // no body - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(1); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(400, '"username" is required')); - - // number as username (userid instead of username) - mockRequest.body.username = 12312312312312; - - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(2); - expect(mockNext.mock.calls.length).toBe(2); - expect(mockNext.mock.calls[1][0]).toEqual(new ApiError(400, '"username" must be a string')); - - // no userid - mockRequest.body.username = mockUser.username; - delete mockRequest.body.userid; - - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(3); - expect(mockNext.mock.calls.length).toBe(3); - expect(mockNext.mock.calls[2][0]).toEqual(new ApiError(400, '"userid" is required')); - - mockRequest.body.userid = '123123123123'; - /* - // invalid user name (to less or many numbers) - mockRequest.body.username = "invalid#123"; - - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(4); - expect(mockNext.mock.calls.length).toBe(4); - expect(mockNext.mock.calls[3][0]).toEqual(new ApiError(400, '"username" with value "invalid#123" fails to match the required pattern: /[\\w\\s]+#[0-9]{4}/')); - - mockRequest.body.username = "invalid#12345"; - - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(5); - expect(mockNext.mock.calls.length).toBe(5); - expect(mockNext.mock.calls[4][0]).toEqual(new ApiError(400, '"username" with value "invalid#12345" fails to match the required pattern: /[\w\s]+#[0-9]{4}/')); - */ - }); - - it('should log error if administrator with same username exists', async () => { - - mockRequest.body = mockUser; - - // Same username - mockAdministrator.userName = mockUser.username; - mockingoose(Administrator).toReturn(mockAdministrator, 'findOne'); - - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(1); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(400, `Administrator ${mockUser.username} already exists`)); - - // Same userid - mockAdministrator.userName = 'testuser#9999' - mockAdministrator.userId = mockUser.userid; - mockingoose(Administrator).toReturn(mockAdministrator, 'findOne'); - - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(2); - expect(mockNext.mock.calls.length).toBe(2); - expect(mockNext.mock.calls[1][0]).toEqual(new ApiError(400, `Administrator with ID ${mockUser.userid} already exists`)); - }); - - it('should save new Administrator and send response if everything is fine', async () => { - - mockRequest.body = mockUser; - mockingoose(Administrator).toReturn(null, 'findOne'); - mockingoose(Administrator).toReturn(null, 'save'); - - await addAdministratorRequest(mockRequest, mockResponse, mockNext); - - expect(mockNext.mock.calls.length).toBe(1); - expect((mockNext.mock.calls[0][0] as ApiSuccess)).toEqual(new ApiSuccess(201)); - - // no error - expect(spyLogger.mock.calls.length).toBe(0); - }); -}); - -describe('administrator/index.ts getAdministratorListRequest', () => { - - let mockRequest: Request; - let mockResponse: Response; - let mockUser: any; - let mockAdministrator: any; - let mockNext: jest.Mock; - let spyLogger: jest.SpyInstance; - - beforeEach(() => { - mockRequest = { - headers: {}, - body: {}, - } as Request; - - mockResponse = { - status: jest.fn(() => mockResponse), - json: jest.fn(), - send: jest.fn(), - end: jest.fn() - } as any as Response; - - mockUser = { - username: 'test#1231', - userid: '123456789123456789', - } - - mockAdministrator = {}; - - mockNext = jest.fn(); - spyLogger = jest.spyOn(loggerFile, 'error'); - - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should log error if database fails', async () => { - mockingoose(Administrator).toReturn(null, 'find'); - - await getAdministratorListRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(1); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(503, `Internal error while retrieving administrators`)); - }); - - it('should return list of administrators', async () => { - mockAdministrator = [ - { - userName: "TestUserName", - userId: "TestUserId", - deletable: true, - createdAt: new Date().valueOf(), - }, - { - userName: "TestUserName", - userId: "TestUserId", - deletable: true, - createdAt: new Date().valueOf(), - }, - ]; - - mockingoose(Administrator).toReturn(mockAdministrator, 'find'); - await getAdministratorListRequest(mockRequest, mockResponse, mockNext); - - expect(spyLogger.mock.calls.length).toBe(0); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiSuccess(200, mockAdministrator)); - - }); - -}); - - -describe('administrator/index.ts deleteAdministratorRequest', () => { - - let mockRequest: Request; - let mockResponse: Response; - let mockUser: any; - let mockAdministrator: any; - let mockNext: jest.Mock; - let spyLogger: jest.SpyInstance; - - beforeEach(() => { - mockRequest = { - headers: {}, - body: {}, - params: {}, - } as Request; - - mockResponse = { - status: jest.fn(() => mockResponse), - json: jest.fn(), - send: jest.fn(), - end: jest.fn() - } as any as Response; - - mockUser = { - username: 'test#1231', - userid: '123456789123456789', - } - - mockAdministrator = {}; - - mockNext = jest.fn(); - spyLogger = jest.spyOn(loggerFile, 'error'); - - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should log error if wrong deleteAdministratorRequestSchema provided', async () => { - - // no body - await deleteAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(1); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(400, '"value" is required')); - - // id too short - mockRequest.params.id = "12312312312312"; - - await deleteAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(2); - expect(mockNext.mock.calls.length).toBe(2); - expect(mockNext.mock.calls[1][0]).toEqual(new ApiError(400, `"value" with value "${mockRequest.params.id}" fails to match the required pattern: /^\\d{18}$/`)); - - // id with letters - mockRequest.params.id = "12312312312312"; - - await deleteAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(3); - expect(mockNext.mock.calls.length).toBe(3); - expect(mockNext.mock.calls[2][0]).toEqual(new ApiError(400, `"value" with value "${mockRequest.params.id}" fails to match the required pattern: /^\\d{18}$/`)); - - }); - - it('should log error if administrator has not been found', async () => { - - mockRequest.params.id = "123456789012345678"; - mockingoose(Administrator).toReturn(null, 'findOne'); - - await deleteAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(1); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(404, `Administrator with id ${mockRequest.params.id} not found in database`)); - - }); - - it('should log error if administrator is not deletable', async () => { - - - mockRequest.params.id = "123456789012345678"; - mockAdministrator.deletable = false; - mockingoose(Administrator).toReturn(mockAdministrator, 'findOne'); - - await deleteAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(1); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(403, `Administrator with id ${mockRequest.params.id} is not deletable`)); - - }); - - it('should return 200 if deletion was successful', async () => { - - mockRequest.params.id = "123456789012345678"; - mockAdministrator.deletable = true; - mockAdministrator.deleteOne = jest.fn(); - mockingoose(Administrator).toReturn(mockAdministrator, 'findOne'); - - await deleteAdministratorRequest(mockRequest, mockResponse, mockNext); - expect(spyLogger.mock.calls.length).toBe(0); - expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiSuccess(204)); - - }); -}); diff --git a/packages/backend/tests/administrator.spec.ts b/packages/backend/tests/administrator.spec.ts new file mode 100644 index 00000000..25e72fca --- /dev/null +++ b/packages/backend/tests/administrator.spec.ts @@ -0,0 +1,214 @@ +import { loggerFile } from "../src/configuration/logger"; +import { adminAdministratorPostRequest, adminAdministratorDeleteRequest, adminAdministratorGetRequest } from "../src/controllers/administrator"; +import mockingoose from 'mockingoose'; +import { Request, Response } from "express"; +import { RegistrationToken } from "../src/controllers/authentication/registrationToken.schema"; +import { Administrator } from '../src/controllers/administrator/administrator.schema'; +import { ApiError, ApiSuccess } from "../src/utils/api"; + +jest.mock('../src/configuration/environment.ts'); + +describe('administrator/index.ts adminAdministratorPostRequest', () => { + + let mockRequest: Request; + let mockResponse: Response; + let mockNext: jest.Mock; + let spyLogger: jest.SpyInstance; + + beforeEach(() => { + mockRequest = { + headers: {}, + body: {}, + } as Request; + + mockResponse = { + status: jest.fn(() => mockResponse), + json: jest.fn(), + send: jest.fn(), + end: jest.fn() + } as any as Response; + + mockNext = jest.fn(); + spyLogger = jest.spyOn(loggerFile, 'error'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return token', async () => { + // This actually does not return null, but it makes the unit test works + mockingoose(RegistrationToken).toReturn(null, 'save'); + + await adminAdministratorPostRequest(mockRequest, mockResponse, mockNext); + + expect(spyLogger.mock.calls.length).toBe(0); + expect(mockNext.mock.calls.length).toBe(1); + expect(mockNext.mock.calls[0][0].data.token).not.toBeFalsy; + expect(mockNext.mock.calls[0][0].data.origin).not.toBeFalsy; + expect(mockNext.mock.calls[0][0].data.lifetime).not.toBeFalsy; + + }); +}); + + +describe('administrator/index.ts adminAdministratorGetRequest', () => { + + let mockRequest: Request; + let mockResponse: Response; + let mockNext: jest.Mock; + let spyLogger: jest.SpyInstance; + + beforeEach(() => { + mockRequest = { + headers: {}, + body: {}, + } as Request; + + mockResponse = { + status: jest.fn(() => mockResponse), + json: jest.fn(), + send: jest.fn(), + end: jest.fn() + } as any as Response; + + mockNext = jest.fn(); + spyLogger = jest.spyOn(loggerFile, 'error'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should log error if database fails', async () => { + mockingoose(Administrator).toReturn(null, 'find'); + + await adminAdministratorGetRequest(mockRequest, mockResponse, mockNext); + expect(spyLogger.mock.calls.length).toBe(1); + expect(mockNext.mock.calls.length).toBe(1); + expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(500, `Internal error while retrieving administrators`)); + }); + + it('should return list of administrators', async () => { + let mockAdministrator = [ + { + username: "TestUserName", + deletable: false, + createdAt: new Date().valueOf(), + }, + { + username: "TestUserName", + deletable: true, + createdAt: new Date().valueOf(), + device: {} + }, + ]; + + mockingoose(Administrator).toReturn(mockAdministrator, 'find'); + await adminAdministratorGetRequest(mockRequest, mockResponse, mockNext); + + expect(spyLogger.mock.calls.length).toBe(0); + expect(mockNext.mock.calls.length).toBe(1); + expect(mockNext.mock.calls[0][0].data.length).toEqual(mockAdministrator.length); + }); + +}); + + +describe('administrator/index.ts adminAdministratorDeleteRequest', () => { + + let mockRequest: Request; + let mockResponse: Response; + let mockNext: jest.Mock; + let spyLogger: jest.SpyInstance; + + beforeEach(() => { + mockRequest = { + headers: {}, + body: {}, + params: {}, + query: {}, + } as Request; + + mockResponse = { + status: jest.fn(() => mockResponse), + json: jest.fn(), + send: jest.fn(), + end: jest.fn() + } as any as Response; + + mockNext = jest.fn(); + spyLogger = jest.spyOn(loggerFile, 'error'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should validate user input', async () => { + const tests = [ + { + // no parameters + prepare: () => { }, + expect: new ApiError(400, '"username" is required') + }, + { + // username not alphanumeric + prepare: () => { mockRequest.params.username = "test#1234"; }, + expect: new ApiError(400, '"username" must only contain alpha-numeric characters') + }]; + + for (let index = 0; index < tests.length; index++) { + // preparation + tests[index].prepare() + + // execute and compare + await adminAdministratorDeleteRequest(mockRequest, mockResponse, mockNext); + expect(spyLogger.mock.calls.length).toBe(index+1); + expect(mockNext.mock.calls.length).toBe(index+1); + expect(mockNext.mock.calls[index][0]).toEqual(tests[index].expect); + }; + }); + + it('should throw error if administrator has not been found', async () => { + + mockRequest.params.username = "useruser"; + mockingoose(Administrator).toReturn(null, 'findOne'); + + await adminAdministratorDeleteRequest(mockRequest, mockResponse, mockNext); + expect(spyLogger.mock.calls.length).toBe(1); + expect(mockNext.mock.calls.length).toBe(1); + expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(404, `Administrator ${mockRequest.params.username} not found`)); + }); + + + it('should throw error if administrator is not deletable', async () => { + mockRequest.params.username = "useruser"; + mockingoose(Administrator).toReturn({ deletable: false }, 'findOne'); + + await adminAdministratorDeleteRequest(mockRequest, mockResponse, mockNext); + expect(spyLogger.mock.calls.length).toBe(1); + expect(mockNext.mock.calls.length).toBe(1); + expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(403, `Administrator ${mockRequest.params.username} is not deletable`)); + + }); + + it.skip('should return 204 if deletion was successful', async () => { + + /** + * Need to skip, because mocking deleteOne does not work. + * Test failed due to timeout :( + */ + mockRequest.params.username = "useruser"; + expect.assertions(1); + const mockAdministrator = { delete: true, deleteOne: () => Promise.resolve("asd") }; + mockingoose(Administrator).toReturn(mockAdministrator, 'findOne'); + mockingoose(Administrator).toReturn("asd", 'deleteOne'); + + await adminAdministratorDeleteRequest(mockRequest, mockResponse, mockNext); + expect(spyLogger.mock.calls.length).toBe(0); + expect(mockNext.mock.calls.length).toBe(1); + expect(mockNext.mock.calls[0][0]).toEqual(new ApiSuccess(204)); + }); +}); + diff --git a/packages/backend/tests/auth.spec.ts b/packages/backend/tests/auth.spec.ts index 0a4db927..e0d819c6 100644 --- a/packages/backend/tests/auth.spec.ts +++ b/packages/backend/tests/auth.spec.ts @@ -7,7 +7,7 @@ import { loggerFile } from '../src/configuration/logger'; import { ApiError } from "../src/utils/api"; import { RegistrationToken } from '../src/controllers/authentication/registrationToken.schema'; import { v4 as uuidv4 } from "uuid"; -import { Authenticator } from '../src/controllers/authentication/authenticator.schema'; +import { IAuthenticatorDocument } from '../src/controllers/administrator/authenticator.schema'; jest.mock('../src/configuration/environment.ts'); jest.mock('../src/configuration/discord.ts'); @@ -31,6 +31,7 @@ describe('auth/index.ts authAttestationGetRequest', () => { let mockResponse: Response; let mockUser: IAdministratorDocument; let mockNext: jest.Mock; + let fakeAuthenticator: IAuthenticatorDocument; let spyLogger: jest.SpyInstance; beforeEach(() => { @@ -49,6 +50,12 @@ describe('auth/index.ts authAttestationGetRequest', () => { end: jest.fn() } as any as Response; + fakeAuthenticator = { + credentialID: Buffer.from('null'), + credentialPublicKey: Buffer.from('null'), + counter: 0 + } as IAuthenticatorDocument; + mockNext = jest.fn(); mockUser = new Administrator({ username : "testuser123" }); @@ -109,7 +116,7 @@ describe('auth/index.ts authAttestationGetRequest', () => { await authAttestationGetRequest(mockRequest, mockResponse, mockNext); expect(spyLogger.mock.calls.length).toBe(1); expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(404, 'Registration token not found')); + expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(404, 'Registration token not found or expired')); }); it('should throw error if user already registered', async () => { @@ -118,7 +125,7 @@ describe('auth/index.ts authAttestationGetRequest', () => { token: uuidv4(), } - mockUser.device = new Authenticator(); + mockUser.device = fakeAuthenticator; mockingoose(Administrator).toReturn(mockUser, 'findOneAndUpdate') mockingoose(RegistrationToken).toReturn(new RegistrationToken(), 'findOne') @@ -151,6 +158,7 @@ describe('auth/index.ts authAttestationPostRequest', () => { let mockResponse: Response; let mockUser: IAdministratorDocument; let mockNext: jest.Mock; + let fakeAuthenticator: IAuthenticatorDocument; let spyLogger: jest.SpyInstance; beforeEach(() => { @@ -171,7 +179,13 @@ describe('auth/index.ts authAttestationPostRequest', () => { mockNext = jest.fn(); - mockUser = new Administrator({ username : "testuser123" }) + mockUser = new Administrator({ username : "testuser123" }); + + fakeAuthenticator = { + credentialID: Buffer.from('null'), + credentialPublicKey: Buffer.from('null'), + counter: 0 + } as IAuthenticatorDocument; spyLogger = jest.spyOn(loggerFile, 'error'); }); @@ -239,7 +253,7 @@ describe('auth/index.ts authAttestationPostRequest', () => { await authAttestationPostRequest(mockRequest, mockResponse, mockNext); expect(spyLogger.mock.calls.length).toBe(1); expect(mockNext.mock.calls.length).toBe(1); - expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(404, 'Registration token not found')); + expect(mockNext.mock.calls[0][0]).toEqual(new ApiError(404, 'Registration token not found or expired')); }); it('should throw error if something went wrong with the user', async () => { @@ -255,7 +269,7 @@ describe('auth/index.ts authAttestationPostRequest', () => { { // user already registered prepare: () => { - mockUser.device = new Authenticator(); + mockUser.device = fakeAuthenticator; mockingoose(Administrator).toReturn(mockUser, 'findOne') }, expect: new ApiError(403, 'User already registered') @@ -306,6 +320,7 @@ describe('auth/index.ts authAssertionGetRequest', () => { let mockResponse: Response; let mockUser: IAdministratorDocument; let mockNext: jest.Mock; + let fakeAuthenticator: IAuthenticatorDocument; let spyLogger: jest.SpyInstance; beforeEach(() => { @@ -328,6 +343,12 @@ describe('auth/index.ts authAssertionGetRequest', () => { mockUser = new Administrator({ username : "testuser123" }) mockUser.save = jest.fn(); + + fakeAuthenticator = { + credentialID: Buffer.from('null'), + credentialPublicKey: Buffer.from('null'), + counter: 0 + } as IAuthenticatorDocument; spyLogger = jest.spyOn(loggerFile, 'error'); }); @@ -405,7 +426,7 @@ describe('auth/index.ts authAssertionGetRequest', () => { username: 'testuser123', } - mockUser.device = new Authenticator({ credentialID: 1 }); + mockUser.device = fakeAuthenticator; mockingoose(Administrator).toReturn(mockUser, 'findOne') await authAssertionGetRequest(mockRequest, mockResponse, mockNext); @@ -421,6 +442,7 @@ describe('auth/index.ts authAssertionPostRequest', () => { let mockResponse: Response; let mockUser: IAdministratorDocument; let mockNext: jest.Mock; + let fakeAuthenticator: IAuthenticatorDocument; let spyLogger: jest.SpyInstance; beforeEach(() => { @@ -441,7 +463,13 @@ describe('auth/index.ts authAssertionPostRequest', () => { mockNext = jest.fn(); - mockUser = new Administrator({ username : "testuser123" }) + mockUser = new Administrator({ username : "testuser123" }); + + fakeAuthenticator = { + credentialID: Buffer.from('null'), + credentialPublicKey: Buffer.from('null'), + counter: 0 + } as IAuthenticatorDocument; spyLogger = jest.spyOn(loggerFile, 'error'); }); @@ -508,7 +536,7 @@ describe('auth/index.ts authAssertionPostRequest', () => { { // user has no active challenge prepare: () => { - mockUser.device = new Authenticator(); + mockUser.device = fakeAuthenticator; mockingoose(Administrator).toReturn(mockUser, 'findOne') }, expect: new ApiError(400, 'User has no pending challenge') diff --git a/packages/frontend/package-lock.json b/packages/frontend/package-lock.json index 902c45bb..5ae525bb 100644 --- a/packages/frontend/package-lock.json +++ b/packages/frontend/package-lock.json @@ -10,10 +10,10 @@ "dependencies": { "@simplewebauthn/browser": "^2.2.1", "axios": "^0.21.1", - "buefy": "^0.8.20", - "bulma": "^0.9.0", + "buefy": "^0.9.7", "core-js": "^3.6.4", - "uuid": "^3.4.0", + "qrcode": "^1.4.4", + "uuid": "^8.3.2", "vue": "^2.6.12", "vue-router": "^3.4.3", "vuelidate": "^0.7.5", @@ -2290,7 +2290,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, "engines": { "node": ">=6" } @@ -2299,7 +2298,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -2775,8 +2773,7 @@ "node_modules/base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "node_modules/batch": { "version": "0.6.1", @@ -3100,22 +3097,20 @@ } }, "node_modules/buefy": { - "version": "0.8.20", - "resolved": "https://registry.npmjs.org/buefy/-/buefy-0.8.20.tgz", - "integrity": "sha512-pg8Cn0m9cjqp2/vaKT4VIfU8KIumuX/gAT1GtearXRs56+kKqAPx3j9O8cm9W6P4jPUCHajKX6H8AqD0ram2Bg==", + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/buefy/-/buefy-0.9.7.tgz", + "integrity": "sha512-Fli0ZjNDgtFtHm0LItWmfhNJ1oLjDwPzUWccvwXXoo2mADXaH8JQxyhY+drUuUV5/GMu5PtwqQSqPgZy942VZg==", "dependencies": { - "bulma": "0.7.5" + "bulma": "0.9.2" }, "engines": { "node": ">= 4.0.0", "npm": ">= 3.0.0" + }, + "peerDependencies": { + "vue": "^2.6.11" } }, - "node_modules/buefy/node_modules/bulma": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz", - "integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw==" - }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -3127,11 +3122,29 @@ "isarray": "^1.0.0" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "node_modules/buffer-indexof": { "version": "1.1.1", @@ -3158,9 +3171,9 @@ "dev": true }, "node_modules/bulma": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz", - "integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.2.tgz", + "integrity": "sha512-e14EF+3VSZ488yL/lJH0tR8mFWiEQVCMi/BQUMi2TGMBOk+zrDg4wryuwm/+dRSHJw0gMawp2tsW7X1JYUCE3A==" }, "node_modules/bytes": { "version": "3.1.0", @@ -3398,7 +3411,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } @@ -3905,7 +3917,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, "dependencies": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", @@ -3915,14 +3926,12 @@ "node_modules/cliui/node_modules/emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, "engines": { "node": ">=4" } @@ -3931,7 +3940,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -3945,7 +3953,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "dependencies": { "ansi-regex": "^4.1.0" }, @@ -4026,7 +4033,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -4034,8 +4040,7 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "node_modules/color-string": { "version": "1.5.3", @@ -4905,7 +4910,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5311,6 +5315,11 @@ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", "dev": true }, + "node_modules/dijkstrajs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz", + "integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs=" + }, "node_modules/dir-glob": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", @@ -7110,7 +7119,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7805,8 +7813,7 @@ "node_modules/ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "node_modules/iferr": { "version": "0.1.5", @@ -11189,7 +11196,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true, "engines": { "node": ">=4" } @@ -11409,6 +11415,14 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/pnp-webpack-plugin": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", @@ -12322,6 +12336,54 @@ "teleport": ">=0.2.0" } }, + "node_modules/qrcode": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz", + "integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==", + "dependencies": { + "buffer": "^5.4.3", + "buffer-alloc": "^1.2.0", + "buffer-from": "^1.1.1", + "dijkstrajs": "^1.0.1", + "isarray": "^2.0.1", + "pngjs": "^3.3.0", + "yargs": "^13.2.4" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/qrcode/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/qrcode/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -12823,11 +12885,19 @@ "node": ">=0.12.0" } }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12835,8 +12905,7 @@ "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "node_modules/requires-port": { "version": "1.0.0", @@ -13276,8 +13345,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "node_modules/set-value": { "version": "2.0.1", @@ -13621,6 +13689,15 @@ "node": ">=0.8.0" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -14932,11 +15009,11 @@ } }, "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "bin": { - "uuid": "bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/v8-compile-cache": { @@ -15574,6 +15651,15 @@ "node": ">= 6" } }, + "node_modules/webpack-log/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/webpack-merge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", @@ -15678,8 +15764,7 @@ "node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "node_modules/wide-align": { "version": "1.1.3", @@ -15755,7 +15840,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -15768,14 +15852,12 @@ "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, "engines": { "node": ">=4" } @@ -15784,7 +15866,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -15798,7 +15879,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "dependencies": { "ansi-regex": "^4.1.0" }, @@ -15857,8 +15937,7 @@ "node_modules/y18n": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, "node_modules/yallist": { "version": "3.1.1", @@ -15870,7 +15949,6 @@ "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, "dependencies": { "cliui": "^5.0.0", "find-up": "^3.0.0", @@ -15888,7 +15966,6 @@ "version": "13.1.2", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -15911,14 +15988,12 @@ "node_modules/yargs/node_modules/emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "node_modules/yargs/node_modules/find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, "dependencies": { "locate-path": "^3.0.0" }, @@ -15930,7 +16005,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, "engines": { "node": ">=4" } @@ -15939,7 +16013,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -15952,7 +16025,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -15964,7 +16036,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, "dependencies": { "p-limit": "^2.0.0" }, @@ -15976,7 +16047,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -15985,7 +16055,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -15999,7 +16068,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "dependencies": { "ansi-regex": "^4.1.0" }, @@ -18178,14 +18246,12 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -18585,8 +18651,7 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "batch": { "version": "0.6.1", @@ -18890,18 +18955,11 @@ } }, "buefy": { - "version": "0.8.20", - "resolved": "https://registry.npmjs.org/buefy/-/buefy-0.8.20.tgz", - "integrity": "sha512-pg8Cn0m9cjqp2/vaKT4VIfU8KIumuX/gAT1GtearXRs56+kKqAPx3j9O8cm9W6P4jPUCHajKX6H8AqD0ram2Bg==", + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/buefy/-/buefy-0.9.7.tgz", + "integrity": "sha512-Fli0ZjNDgtFtHm0LItWmfhNJ1oLjDwPzUWccvwXXoo2mADXaH8JQxyhY+drUuUV5/GMu5PtwqQSqPgZy942VZg==", "requires": { - "bulma": "0.7.5" - }, - "dependencies": { - "bulma": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz", - "integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw==" - } + "bulma": "0.9.2" } }, "buffer": { @@ -18915,11 +18973,29 @@ "isarray": "^1.0.0" } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-indexof": { "version": "1.1.1", @@ -18946,9 +19022,9 @@ "dev": true }, "bulma": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz", - "integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.2.tgz", + "integrity": "sha512-e14EF+3VSZ488yL/lJH0tR8mFWiEQVCMi/BQUMi2TGMBOk+zrDg4wryuwm/+dRSHJw0gMawp2tsW7X1JYUCE3A==" }, "bytes": { "version": "3.1.0", @@ -19139,8 +19215,7 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "camelcase-keys": { "version": "2.1.0", @@ -19557,7 +19632,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, "requires": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", @@ -19567,20 +19641,17 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -19591,7 +19662,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -19656,7 +19726,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -19664,8 +19733,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { "version": "1.5.3", @@ -20409,8 +20477,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", @@ -20736,6 +20803,11 @@ } } }, + "dijkstrajs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz", + "integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs=" + }, "dir-glob": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", @@ -22262,8 +22334,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-func-name": { "version": "2.0.0", @@ -22846,8 +22917,7 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "iferr": { "version": "0.1.5", @@ -25600,8 +25670,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", @@ -25771,6 +25840,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, "pnp-webpack-plugin": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", @@ -26574,6 +26648,36 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true }, + "qrcode": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz", + "integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==", + "requires": { + "buffer": "^5.4.3", + "buffer-alloc": "^1.2.0", + "buffer-from": "^1.1.1", + "dijkstrajs": "^1.0.1", + "isarray": "^2.0.1", + "pngjs": "^3.3.0", + "yargs": "^13.2.4" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + } + } + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -26962,6 +27066,14 @@ "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "request-promise-core": { @@ -26987,14 +27099,12 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "requires-port": { "version": "1.0.0", @@ -27382,8 +27492,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -27645,6 +27754,14 @@ "faye-websocket": "^0.10.0", "uuid": "^3.4.0", "websocket-driver": "0.6.5" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "sockjs-client": { @@ -28796,9 +28913,9 @@ "dev": true }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.1.0", @@ -29347,6 +29464,14 @@ "requires": { "ansi-colors": "^3.0.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "webpack-merge": { @@ -29429,8 +29554,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { "version": "1.1.3", @@ -29493,7 +29617,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, "requires": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -29503,20 +29626,17 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -29527,7 +29647,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -29576,8 +29695,7 @@ "y18n": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, "yallist": { "version": "3.1.1", @@ -29589,7 +29707,6 @@ "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, "requires": { "cliui": "^5.0.0", "find-up": "^3.0.0", @@ -29606,14 +29723,12 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, "requires": { "locate-path": "^3.0.0" } @@ -29621,14 +29736,12 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -29638,7 +29751,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -29647,7 +29759,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, "requires": { "p-limit": "^2.0.0" } @@ -29655,14 +29766,12 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -29673,7 +29782,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -29684,7 +29792,6 @@ "version": "13.1.2", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 72b66619..29cc93c3 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -16,10 +16,10 @@ "dependencies": { "@simplewebauthn/browser": "^2.2.1", "axios": "^0.21.1", - "buefy": "^0.8.20", - "bulma": "^0.9.0", + "buefy": "^0.9.7", "core-js": "^3.6.4", - "uuid": "^3.4.0", + "qrcode": "^1.4.4", + "uuid": "^8.3.2", "vue": "^2.6.12", "vue-router": "^3.4.3", "vuelidate": "^0.7.5", diff --git a/packages/frontend/src/components/administration/AdministratorList.vue b/packages/frontend/src/components/administration/AdministratorList.vue index 5223a6a3..bf2ce3c8 100644 --- a/packages/frontend/src/components/administration/AdministratorList.vue +++ b/packages/frontend/src/components/administration/AdministratorList.vue @@ -8,127 +8,98 @@ - + > + + {{ props.row.username }} + + + + {{ new Date(props.row.createdAt).toLocaleString() }} + + + + + {{ props.row.hasDevice ? 'Registriert' : 'Nicht gefunden!' }} + + + + + + Löschen + +

+ >Neuen Registrierungstoken erstellen
+ diff --git a/packages/frontend/src/components/administration/RegistrationTokenModal.vue b/packages/frontend/src/components/administration/RegistrationTokenModal.vue new file mode 100644 index 00000000..539c97f7 --- /dev/null +++ b/packages/frontend/src/components/administration/RegistrationTokenModal.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/packages/frontend/src/main.js b/packages/frontend/src/main.js index 42470806..dd65174b 100644 --- a/packages/frontend/src/main.js +++ b/packages/frontend/src/main.js @@ -9,7 +9,7 @@ import store from './store'; import './fmdb.scss'; Vue.use(Vuelidate); -Vue.use(Buefy); +Vue.use(Buefy, { defaultIconPack: 'fas' }); Vue.prototype.$http = Axios; diff --git a/packages/frontend/src/store/index.js b/packages/frontend/src/store/index.js index f721fad7..8e51c85b 100644 --- a/packages/frontend/src/store/index.js +++ b/packages/frontend/src/store/index.js @@ -135,49 +135,6 @@ export default new Vuex.Store({ }); }, - requestToken({ commit }, username) { - commit('SET_AUTH'); - return new Promise((resolve, reject) => { - ApiUtil.post('/token', { username }) - .then(({ data: apiResponse }) => { - if (apiResponse.status === 'success') { - commit('SET_AUTH', null); - resolve(); - } else { - commit('SET_AUTH', new Error(apiResponse.error[0].message)); - reject(apiResponse); - } - }) - .catch(() => { - commit('SET_AUTH', new Error()); - reject(); - }); - }); - }, - - loginWithToken({ commit }, credentials) { - commit('SET_AUTH'); - return new Promise((resolve, reject) => { - ApiUtil.post('/login', credentials) - .then(({ data: apiResponse }) => { - if (apiResponse.status === 'success') { - const jwt = apiResponse.data.accesstoken; - ApiUtil.defaults.headers.common.Authorization = `Bearer ${jwt}`; - localStorage.setItem('token', jwt); - commit('SET_AUTH', jwt); - resolve(); - } else { - commit('SET_AUTH', new Error(apiResponse.error[0].message)); - reject(apiResponse); - } - }) - .catch(() => { - commit('SET_AUTH', new Error()); - reject(); - }); - }); - }, - logout(context) { context.commit('SET_AUTH', null); ApiUtil.defaults.headers.common.Authorization = ''; diff --git a/packages/frontend/src/store/modules/administration.js b/packages/frontend/src/store/modules/administration.js index 753f8606..79392180 100644 --- a/packages/frontend/src/store/modules/administration.js +++ b/packages/frontend/src/store/modules/administration.js @@ -3,66 +3,69 @@ import StoreUtil from '../utils/StoreUtil'; export default { state: { - changeRequest: StoreUtil.state(), administrators: StoreUtil.state(), }, mutations: { - SET_CHANGE_REQUEST(state, payload) { - state.newAdministrator = StoreUtil.updateState(state.changeRequest, payload); - }, SET_ADMINISTRATORS(state, payload) { state.administrators = StoreUtil.updateState(state.administrators, payload); }, + UNLOCK_ADMINISTRATORS(state, payload) { + state.administrators = StoreUtil.unlockState(state.administrators, payload); + }, + LOCK_ADMINISTRATORS(state) { + state.administrators = StoreUtil.unlockState(state.administrators); + }, }, actions: { - addAdministrator({ commit }, administratorObject) { - commit('SET_CHANGE_REQUEST'); + requestToken({ commit }) { + commit('LOCK_ADMINISTRATORS'); return new Promise((resolve, reject) => { - ApiUtil.post('/settings/administrator', administratorObject) + ApiUtil.post('/settings/administrators') .then(({ data: apiResponse }) => { if (apiResponse.status === 'success') { - commit('SET_CHANGE_REQUEST', {}); - resolve(); + commit('UNLOCK_ADMINISTRATORS', true); + resolve(apiResponse); } else { - commit('SET_CHANGE_REQUEST', new Error(apiResponse.error[0].message)); + commit('SET_ADMINISTRATORS', new Error(apiResponse.error[0].message)); reject(apiResponse); } }) .catch((err) => { console.log(err); - commit('SET_CHANGE_REQUEST', err); + commit('SET_ADMINISTRATORS', err); reject(err); }); }); }, - deleteAdministrator({ commit }, administratorId) { - commit('SET_CHANGE_REQUEST'); + deleteAdministrator({ commit }, username) { + commit('LOCK_ADMINISTRATORS'); return new Promise((resolve, reject) => { - ApiUtil.delete(`/settings/administrator/${administratorId}`) + ApiUtil.delete(`/settings/administrators/${username}`) .then(({ status, data: apiResponse }) => { - /** - * Axios doesn't pass the response body of a successful - * delete operation. - */ + /** + * Axios doesn't pass the response body of a successful + * delete operation. + */ if (status === 204 || apiResponse.status === 'success') { - commit('SET_CHANGE_REQUEST', {}); + commit('UNLOCK_ADMINISTRATORS', true); resolve(); } else { - commit('SET_CHANGE_REQUEST', new Error(apiResponse.error[0].message)); + // Do not use SET_ADMINISTRATORS to avoid empty table + commit('UNLOCK_ADMINISTRATORS', false); reject(apiResponse); } }) .catch((err) => { console.log(err); - commit('SET_CHANGE_REQUEST', err); + commit('SET_ADMINISTRATORS', err); reject(); }); }); }, getAdministrators({ commit }) { - commit('SET_ADMINISTRATORS'); + commit('LOCK_ADMINISTRATORS'); return new Promise((resolve, reject) => { - ApiUtil.get('/settings/administrator') + ApiUtil.get('/settings/administrators') .then(({ data: apiResponse }) => { if (apiResponse.status === 'success') { commit('SET_ADMINISTRATORS', apiResponse.data); @@ -81,7 +84,6 @@ export default { }, }, getters: { - administratorChangeRequestGetStatus: (state) => state.changeRequest.status, administratorListGetData: (state) => state.administrators.data, administratorListGetStatus: (state) => state.administrators.status, }, diff --git a/packages/frontend/src/store/utils/StoreUtil.js b/packages/frontend/src/store/utils/StoreUtil.js index de418385..44e68748 100644 --- a/packages/frontend/src/store/utils/StoreUtil.js +++ b/packages/frontend/src/store/utils/StoreUtil.js @@ -14,12 +14,12 @@ export default class StoreUtil { }, }; } + /** * @param {Object} state - the state to update * @param {Object,Error} data - data to update with * @returns {Object} state after update */ - static updateState(state, data = undefined) { if (!state) throw new Error('state object is missing'); @@ -39,12 +39,55 @@ export default class StoreUtil { : this._mutationSuccess({ ...state }, data); } + /** + * Unlocks state and clears error depending on success parameter + * @param {Object} state - the state to update + * @param {Boolean} success - set to success state and clear error + * @returns {Object} state after update + */ + static unlockState(state, success) { + return this._mutationUnlock({ ...state }, success); + } + + /** + * Locks state without resetting stored data + * @param {Object} state - the state to update + * @returns {Object} state after update + */ + static lockState(state) { + return this._mutationLock({ ...state }); + } + + /** + * @param {Object} state - the state to be unlocked + * @param {Boolean} success - set to success state + * @returns {Object} updated state + */ + static _mutationUnlock(state, success) { + state.status.pending = false; + state.status.success = success; + state.status.fail = !success; + if (!success) state.status.error = null; + return state; + } + + /** + * @param {Object} state - the state to be locked + * @returns {Object} updated state + */ + static _mutationLock(state) { + state.status.pending = true; + state.status.success = false; + state.status.fail = false; + return state; + } + /** * @param {Object} state - the status to be put in pending state * @returns {Object} updated state */ static _mutationPending(state) { - // state.data = null; + state.data = null; state.status.pending = true; state.status.success = false; state.status.fail = false; diff --git a/packages/frontend/src/views/Dashboard.vue b/packages/frontend/src/views/Dashboard.vue index 3e6d0687..036dec4f 100644 --- a/packages/frontend/src/views/Dashboard.vue +++ b/packages/frontend/src/views/Dashboard.vue @@ -4,15 +4,10 @@
- + - - @@ -27,8 +22,7 @@