move migration, bugfixes, ....
This commit is contained in:
		
							parent
							
								
									5401e2594d
								
							
						
					
					
						commit
						18863bee7f
					
				
					 25 changed files with 255 additions and 33 deletions
				
			
		|  | @ -1,4 +1,5 @@ | |||
| recursive-include lenticular_cloud/template * | ||||
| recursive-include lenticular_cloud/static ** | ||||
| recursive-include lenticular_cloud/migrations ** | ||||
| include lenticular_cloud/*.cfg | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import 'jquery-form' | |||
| import {ConfirmDialog, Dialog} from './confirm-modal.js'; | ||||
| 
 | ||||
| jQuery = window.$ = window.jQuery = require('jquery'); | ||||
| var cbor = require('cbor-web'); | ||||
| var forge = require('node-forge'); | ||||
| var QRCode = require("qrcode-svg"); | ||||
| var pki = require('node-forge/lib/pki'); | ||||
|  | @ -16,6 +17,8 @@ const $ = document.querySelector.bind(document); | |||
| const $$ = document.querySelectorAll.bind(document); | ||||
| 
 | ||||
| 
 | ||||
| window.CBOR = cbor; | ||||
| 
 | ||||
| /* | ||||
| Convert  an ArrayBuffer into a string | ||||
| from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
 | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import subprocess | |||
| from ory_hydra_client import Client | ||||
| import os | ||||
| 
 | ||||
| from pathlib import Path | ||||
| from ldap3 import Connection, Server, ALL | ||||
| 
 | ||||
| from . import model | ||||
|  | @ -32,13 +33,14 @@ def create_app() -> Flask: | |||
| 
 | ||||
|     #app.ldap_orm = Connection(app.config['LDAP_URL'], app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True) | ||||
|     server = Server(app.config['LDAP_URL'], get_info=ALL) | ||||
|     app.ldap_conn = Connection(server, app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind="DEFAULT") | ||||
|     app.ldap_conn = Connection(server, app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PW'], auto_bind=True) # TODO auto_bind read docu | ||||
|     model.ldap_conn = app.ldap_conn | ||||
|     model.base_dn = app.config['LDAP_BASE_DN'] | ||||
| 
 | ||||
|     from .model import db, migrate | ||||
|     db.init_app(app) | ||||
|     migrate.init_app(app, db) | ||||
|     migration_dir = Path(app.root_path) / 'migrations' | ||||
|     migrate.init_app(app, db, directory=str(migration_dir)) | ||||
| #    with app.app_context(): | ||||
| #        db.create_all() | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ PREFERRED_URL_SCHEME = 'https' | |||
| 
 | ||||
| DATA_FOLDER = "../data" | ||||
| 
 | ||||
| SCRIPT_LOCATION="lenticular_cloud:migrations" | ||||
| SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATA_FOLDER}/db.sqlite' | ||||
| SQLALCHEMY_TRACK_MODIFICATIONS=False | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ from .model import db, User, UserSignUp | |||
| from .app import create_app | ||||
| from werkzeug.middleware.proxy_fix import ProxyFix | ||||
| from flask_migrate import upgrade | ||||
| from pathlib import Path | ||||
| 
 | ||||
| import logging | ||||
| import os | ||||
|  | @ -78,7 +79,9 @@ def cli_run(app, args): | |||
|     app.run(debug=True, host='127.0.0.1', port=5000) | ||||
| 
 | ||||
| def cli_db_upgrade(args): | ||||
|     upgrade() | ||||
|     app = create_app() | ||||
|     migration_dir = Path(app.root_path) / 'migrations' | ||||
|     upgrade( str(migration_dir) ) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ def upgrade(): | |||
|     # ### commands auto generated by Alembic - please adjust! ### | ||||
|     op.create_table('webauthn_credential', | ||||
|     sa.Column('id', sa.Integer(), nullable=False), | ||||
|     sa.Column('user_id', sa.Integer(), nullable=False), | ||||
|     sa.Column('user_id', sa.String(length=36), nullable=False), | ||||
|     sa.Column('user_handle', sa.String(length=64), nullable=False), | ||||
|     sa.Column('credential_data', sa.LargeBinary(), nullable=False), | ||||
|     sa.Column('name', sa.String(length=250), nullable=True), | ||||
|  | @ -340,7 +340,7 @@ class WebauthnCredential(db.Model):  # pylint: disable=too-few-public-methods | |||
|     """Webauthn credential model""" | ||||
| 
 | ||||
|     id = db.Column(db.Integer, primary_key=True) | ||||
|     user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) | ||||
|     user_id = db.Column(db.String(length=36), db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False) | ||||
|     user_handle = db.Column(db.String(64), nullable=False) | ||||
|     credential_data = db.Column(db.LargeBinary, nullable=False) | ||||
|     name = db.Column(db.String(250)) | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -51,6 +51,8 @@ | |||
|  * Date: 2021-03-02T17:08Z | ||||
|  */ | ||||
| 
 | ||||
| /*! For license information please see cbor.js.LICENSE.txt */ | ||||
| 
 | ||||
| /**! | ||||
|  * @fileOverview Kickass library to create and place poppers near their reference elements. | ||||
|  * @version 1.16.1 | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										30
									
								
								lenticular_cloud/template/frontend/webauthn_list.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lenticular_cloud/template/frontend/webauthn_list.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| {% extends 'frontend/base.html.j2' %} | ||||
| 
 | ||||
| {% block content %} | ||||
| <div class="users"> | ||||
| 	<h1>WebauthnCredentials list</h1> | ||||
| 
 | ||||
| 	<table class="table"> | ||||
| 		<thead> | ||||
| 			<tr> | ||||
| 				<th>user.username</th> | ||||
| 				<th>user_handle</th> | ||||
| 				<th>credential_data</th> | ||||
| 				<th>name</th> | ||||
| 				<th>_actions</th> | ||||
| 			</tr> | ||||
| 		</thead> | ||||
| 		<tbody> | ||||
| 		{% for cred in creds %} | ||||
| 			<tr> | ||||
| 				<td>{{ cred.user.username }}</td> | ||||
| 				<td>{{ cred.user_handle }}</td> | ||||
| 				<td>{{ cred.credential_data[0:40] }}...</td> | ||||
| 				<td>{{ cred.name }}</td> | ||||
|         <td>{{ render_form(button_form, action_url=url_for('app.webauthn_delete_route', webauthn_id=cred.id)) }}</td> | ||||
| 			</tr> | ||||
| 		{% endfor %} | ||||
| 		</tbody> | ||||
| 	</table> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										140
									
								
								lenticular_cloud/template/frontend/webauthn_register.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								lenticular_cloud/template/frontend/webauthn_register.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | |||
| {#- This file is part of sner4 project governed by MIT license, see the LICENSE.txt file. -#} | ||||
| {% extends 'frontend/base.html.j2' %} | ||||
| 
 | ||||
| {% block script %} | ||||
| <script> | ||||
|   /** | ||||
| 		 * decode base64 data to ArrayBuffer | ||||
| 		 * | ||||
| 		 * @param  {string}      data data to decode | ||||
| 		 * @return {ArrayBuffer}      decoded data | ||||
| 		 */ | ||||
| 		function base64_to_array_buffer(data) { | ||||
| 			return Uint8Array.from(atob(data), c => c.charCodeAt(0)).buffer; | ||||
|     } | ||||
| 
 | ||||
| 	/** | ||||
| 	 * request publicKeyCredentialCreationOptions for webauthn from server | ||||
| 	 * | ||||
| 	 * @return {Promise<Object>} A promise that resolves with publicKeyCredentialCreationOptions for navigator.credentials.create() | ||||
| 	 */ | ||||
| 	function get_pkcco() { | ||||
| 		return fetch("{{ url_for('frontend.webauthn_pkcco_route')}}", {method:'post',  headers: {'Content-Type': 'application/json'}}) | ||||
|       .then(function(resp) { | ||||
|         return resp.text(); | ||||
|       }) | ||||
|       .then(function(data){ | ||||
|         var pkcco = CBOR.decode(base64_to_array_buffer(data)); | ||||
|         console.debug('credentials.create options:', pkcco); | ||||
|          | ||||
| var publicKey = { | ||||
|   // The challenge is produced by the server; see the Security Considerations | ||||
|   challenge: new Uint8Array([21,31,105 /* 29 more random bytes generated by the server */]), | ||||
| 
 | ||||
|   // Relying Party: | ||||
|   rp: { | ||||
|     name: "Lenticular Cloud - domain TODO" | ||||
|   }, | ||||
| 
 | ||||
|   // User: | ||||
|   user: { | ||||
|     id: Uint8Array.from(window.atob("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII="), c=>c.charCodeAt(0)), | ||||
|     name: "{user.domain}", | ||||
|     displayName: "{user.name}", | ||||
|   }, | ||||
| 
 | ||||
|   // This Relying Party will accept either an ES256 or RS256 credential, but | ||||
|   // prefers an ES256 credential. | ||||
|   pubKeyCredParams: [ | ||||
|     { | ||||
|       type: "public-key", | ||||
|       alg: -7 // "ES256" as registered in the IANA COSE Algorithms registry | ||||
|     }, | ||||
|     { | ||||
|       type: "public-key", | ||||
|       alg: -257 // Value registered by this specification for "RS256" | ||||
|     } | ||||
|   ], | ||||
| 
 | ||||
|   authenticatorSelection: { | ||||
|     // Try to use UV if possible. This is also the default. | ||||
|     userVerification: "preferred" | ||||
|   }, | ||||
| 
 | ||||
|   timeout: 360000,  // 6 minutes | ||||
|   excludeCredentials: [ | ||||
|     // Don’t re-register any authenticator that has one of these credentials | ||||
|     //{"id": Uint8Array.from(window.atob("E/e1dhZc++mIsz4f9hb6NifAzJpF1V4mEtRlIPBiWdY="), c=>c.charCodeAt(0)), "type": "public-key"} | ||||
|   ], | ||||
| 
 | ||||
|   // Make excludeCredentials check backwards compatible with credentials registered with U2F | ||||
|   extensions: {"appidExclude": "https://acme.example.com"} | ||||
| }; | ||||
| 
 | ||||
|          | ||||
| return { "publicKey": publicKey }; | ||||
|       }) | ||||
|       .catch(function(error) { console.log('cant get pkcco ',error)}); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * pack attestation | ||||
| 	 * | ||||
| 	 * @param {object} attestation attestation response for the credential to register | ||||
| 	 */ | ||||
| 	function pack_attestation(attestation) { | ||||
| 		console.debug('new credential attestation:', attestation); | ||||
| 
 | ||||
| 		var attestation_data = { | ||||
| 			'clientDataJSON': new Uint8Array(attestation.response.clientDataJSON), | ||||
| 			'attestationObject': new Uint8Array(attestation.response.attestationObject) | ||||
| 		}; | ||||
|     //var form = $('#webauthn_register_form')[0]; | ||||
|     var form = document.querySelector('form') | ||||
|     var base64 = btoa(new Uint8Array(CBOR.encode(attestation_data)).reduce((data, byte) => data + String.fromCharCode(byte), '')); | ||||
| 		form.attestation.value = base64; | ||||
| 		form.submit.disabled = false; | ||||
|     //form.querySelecotr('p[name="attestation_data_status"]').innerHTML = '<span style="color: green;">Prepared</span>'; | ||||
| 	} | ||||
| 
 | ||||
|   console.log(window.PublicKeyCredential ? 'WebAuthn supported' : 'WebAuthn NOT supported'); | ||||
| 
 | ||||
| 
 | ||||
|   get_pkcco() | ||||
|   .then(pkcco => navigator.credentials.create(pkcco)) | ||||
|   .then(attestation_response => pack_attestation(attestation_response)) | ||||
|   .catch(function(error) { | ||||
|     //toastr.error('Registration data preparation failed.'); | ||||
|     console.log(error.message); | ||||
|   }); | ||||
| </script> | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| <div class="profile"> | ||||
| 	<h1>Register new Webauthn credential</h1> | ||||
| 
 | ||||
| 	<div> | ||||
| 		To register new credential: | ||||
| 		<ol> | ||||
| 			<li>Insert/connect authenticator and verify user presence.</li> | ||||
| 			<li>Optionaly set comment for the new credential.</li> | ||||
| 			<li>Submit the registration.</li> | ||||
| 		</ol> | ||||
| 	</div> | ||||
| 
 | ||||
|   {{ render_form(form) }} | ||||
|   {# | ||||
| 	<form id="webauthn_register_form" class="form-horizontal" method="post"> | ||||
| 		{{ form.csrf_token }} | ||||
| 		<div class="form-group"> | ||||
| 			<label class="col-sm-2 control-label">Registration data</label> | ||||
| 			<div class="col-sm-10"><p class="form-control-static" name="attestation_data_status"><span style="color: orange;">To be prepared</span></p></div> | ||||
| 		</div> | ||||
| 		{{ b_wtf.bootstrap_field(form.attestation, horizontal=True) }} | ||||
| 		{{ b_wtf.bootstrap_field(form.name, horizontal=True) }} | ||||
| 		{{ b_wtf.bootstrap_field(form.submit, horizontal=True) }} | ||||
|   </form> | ||||
|   #} | ||||
| </div> | ||||
| {% endblock %} | ||||
|  | @ -33,7 +33,9 @@ | |||
| </div> | ||||
| <script type="application/javascript" src="/static/main.js?v={{ GIT_HASH }}" ></script> | ||||
| <script type="application/javascript" > | ||||
| {% block script_js %}{% endblock %} | ||||
|   {% block script_js %} | ||||
|   // depricated | ||||
| {% endblock %} | ||||
| window.fieldlist = { | ||||
| 	add: function(linkTag) { | ||||
| 
 | ||||
|  | @ -55,6 +57,7 @@ window.fieldlist = { | |||
| 	} | ||||
| }; | ||||
| </script> | ||||
| {% block script %}{% endblock %} | ||||
| </body> | ||||
| </html> | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ from flask import jsonify | |||
| from flask.typing import ResponseReturnValue | ||||
| from flask_login import current_user, logout_user | ||||
| from oauthlib.oauth2.rfc6749.errors import TokenExpiredError | ||||
| from authlib.integrations.base_client.errors import InvalidTokenError | ||||
| from ory_hydra_client.api.admin import list_o_auth_2_clients, get_o_auth_2_client, update_o_auth_2_client, create_o_auth_2_client  | ||||
| from ory_hydra_client.models import OAuth2Client, GenericError | ||||
| from typing import Optional | ||||
|  | @ -28,7 +29,7 @@ def before_request() -> Optional[ResponseReturnValue]: | |||
|             return redirect_login() | ||||
|         if 'groups' not in data or 'admin' not in data['groups']: | ||||
|             return 'Not an admin', 403 | ||||
|     except MissingTokenError: | ||||
|     except (MissingTokenError, InvalidTokenError): | ||||
|         return redirect_login() | ||||
|     return None | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ from flask.typing import ResponseReturnValue | |||
| 
 | ||||
| from flask import Blueprint, render_template, request, url_for | ||||
| import logging | ||||
| import httpx | ||||
| 
 | ||||
| from ..model import User | ||||
| from ..auth_providers import LdapAuthProvider | ||||
|  | @ -18,20 +19,43 @@ from ory_hydra_client.models import GenericError | |||
| 
 | ||||
| api_views = Blueprint('api', __name__, url_prefix='/api') | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| @api_views.route('/users', methods=['GET']) | ||||
| def user_list() -> ResponseReturnValue: | ||||
|     if 'authorization' not in request.headers: | ||||
|         return '', 403 | ||||
|     token = request.headers['authorization'].replace('Bearer ', '') | ||||
|     token_info = introspect_o_auth_2_token.sync(_client=hydra_service.hydra_client) | ||||
| #   if 'authorization' not in request.headers: | ||||
| #       return '', 403 | ||||
| #   token = request.headers['authorization'].replace('Bearer ', '') | ||||
| #   token_info = introspect_o_auth_2_token.sync(_client=hydra_service.hydra_client) | ||||
| 
 | ||||
|     if token_info is None or isinstance(token_info, GenericError): | ||||
|         return 'internal errror', 500 | ||||
| #   if token_info is None or isinstance(token_info, GenericError): | ||||
| #       return 'internal errror', 500 | ||||
| 
 | ||||
|     if not isinstance(token_info.scope, str) or 'lc_i_userlist' not in token_info.scope.split(' '): | ||||
|         return '', 403 | ||||
| #   if not isinstance(token_info.scope, str) or 'lc_i_userlist' not in token_info.scope.split(' '): | ||||
| #       return '', 403 | ||||
| 
 | ||||
|     return jsonify([ | ||||
|             {'username': str(user.username), 'email': str(user.email)} | ||||
|             for user in User.query_().all()]) | ||||
| 
 | ||||
| @api_views.route('/introspect', methods=['POST']) | ||||
| def introspect() -> ResponseReturnValue: | ||||
|     token = request.form['token'] | ||||
|     logger.error(f'debug token: {token}') | ||||
|     resp = httpx.post("https://hydra.cloud.tux.ac/oauth2/introspect", data={'token':token}) | ||||
|     #if token_info is None or isinstance(token_info, GenericError): | ||||
|     if resp.status_code != 200: | ||||
|         return jsonify({}), 500 | ||||
|     token_info = resp.json() | ||||
|     #token_info = introspect_o_auth_2_token.sync(_client=hydra_service, token=token) | ||||
|     logger.error(f'debug hydra: {token_info}') | ||||
| 
 | ||||
|     if not token_info['active']: | ||||
|         return jsonify({'active': False}) | ||||
|     token_info['email'] = token_info['ext']['email'] | ||||
| 
 | ||||
|     logger.error(f'debug: {token_info}') | ||||
| 
 | ||||
|     return jsonify(token_info) | ||||
| 
 | ||||
|  |  | |||
|  | @ -64,6 +64,7 @@ async def consent() -> ResponseReturnValue: | |||
|         token_data = { | ||||
|             'name': str(user.username), | ||||
|             'preferred_username': str(user.username), | ||||
|             'username': str(user.username), | ||||
|             'email': str(user.email), | ||||
|             'email_verified': True, | ||||
|             'groups': [group.name for group in user.groups] | ||||
|  |  | |||
|  | @ -175,7 +175,7 @@ def webauthn_list_route() -> ResponseReturnValue: | |||
|     """list registered credentials for current user""" | ||||
| 
 | ||||
|     creds = WebauthnCredential.query.all() | ||||
|     return render_template('webauthn_list.html', creds=creds, button_form=ButtonForm()) | ||||
|     return render_template('frontend/webauthn_list.html', creds=creds, button_form=ButtonForm()) | ||||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/webauthn/delete/<webauthn_id>', methods=['POST']) | ||||
|  | @ -207,19 +207,15 @@ def random_string(length=32) -> str: | |||
| def webauthn_pkcco_route() -> ResponseReturnValue: | ||||
|     """get publicKeyCredentialCreationOptions""" | ||||
| 
 | ||||
|     form = ButtonForm() | ||||
|     if form.validate_on_submit(): | ||||
|         user = User.query.get(current_user.id) | ||||
|         user_handle = random_string() | ||||
|         exclude_credentials = webauthn_credentials(user) | ||||
|         pkcco, state = webauthn.register_begin( | ||||
|             {'id': user_handle.encode('utf-8'), 'name': user.username, 'displayName': user.username}, | ||||
|             exclude_credentials) | ||||
|         session['webauthn_register_user_handle'] = user_handle | ||||
|         session['webauthn_register_state'] = state | ||||
|         return Response(b64encode(cbor.encode(pkcco)).decode('utf-8'), mimetype='text/plain') | ||||
| 
 | ||||
|     return '', HTTPStatus.BAD_REQUEST | ||||
|     user = User.query.get(current_user.id) | ||||
|     user_handle = random_string() | ||||
|     exclude_credentials = webauthn_credentials(user) | ||||
|     pkcco, state = webauthn.register_begin( | ||||
|         {'id': user_handle.encode('utf-8'), 'name': user.username, 'displayName': user.username}, | ||||
|         exclude_credentials) | ||||
|     session['webauthn_register_user_handle'] = user_handle | ||||
|     session['webauthn_register_state'] = state | ||||
|     return Response(b64encode(cbor.encode(pkcco)).decode('utf-8'), mimetype='text/plain') | ||||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/webauthn/register', methods=['GET', 'POST']) | ||||
|  | @ -248,7 +244,7 @@ def webauthn_register_route() -> ResponseReturnValue: | |||
|             current_app.logger.exception(e) | ||||
|             flash('Error during registration.', 'error') | ||||
| 
 | ||||
|     return render_template('webauthn_register.html', form=form) | ||||
|     return render_template('frontend/webauthn_register.html', form=form) | ||||
| 
 | ||||
| 
 | ||||
| @frontend_views.route('/password_change') | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ from typing import Optional | |||
| from ..model import User, SecurityUser | ||||
| 
 | ||||
| def fetch_token(name: str) -> Optional[dict]: | ||||
|     token = session['token'] | ||||
|     token = session.get('token', None) | ||||
|     if isinstance(token, dict): | ||||
|         return token | ||||
|     return None | ||||
|  |  | |||
							
								
								
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -11,6 +11,7 @@ | |||
|       "dependencies": { | ||||
|         "@fortawesome/fontawesome-free": "*", | ||||
|         "bootstrap": "^4.6.1", | ||||
|         "cbor-web": "*", | ||||
|         "css-loader": "*", | ||||
|         "css-minimizer-webpack-plugin": "*", | ||||
|         "file-loader": "*", | ||||
|  | @ -450,6 +451,14 @@ | |||
|         "url": "https://opencollective.com/browserslist" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/cbor-web": { | ||||
|       "version": "8.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-8.1.0.tgz", | ||||
|       "integrity": "sha512-2hWHHMVrfffgoEmsAUh8vCxHoLa1vgodtC73+C5cSarkJlwTapnqAzcHINlP6Ej0DXuP4OmmJ9LF+JaNM5Lj/g==", | ||||
|       "engines": { | ||||
|         "node": ">=12.19" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/chalk": { | ||||
|       "version": "4.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", | ||||
|  | @ -3279,6 +3288,11 @@ | |||
|       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", | ||||
|       "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==" | ||||
|     }, | ||||
|     "cbor-web": { | ||||
|       "version": "8.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-8.1.0.tgz", | ||||
|       "integrity": "sha512-2hWHHMVrfffgoEmsAUh8vCxHoLa1vgodtC73+C5cSarkJlwTapnqAzcHINlP6Ej0DXuP4OmmJ9LF+JaNM5Lj/g==" | ||||
|     }, | ||||
|     "chalk": { | ||||
|       "version": "4.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ | |||
|   "dependencies": { | ||||
|     "@fortawesome/fontawesome-free": "*", | ||||
|     "bootstrap": "^4.6.1", | ||||
|     "cbor-web": "*", | ||||
|     "css-loader": "*", | ||||
|     "css-minimizer-webpack-plugin": "*", | ||||
|     "file-loader": "*", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue