Added Roles, User Management, fixed bugs, added FlexTable component (should be renamed to GridTable), other table components and test code should be removed down the line, added admin function to fix broken data structures.

This commit is contained in:
2022-05-17 11:06:15 -07:00
parent 038c68f618
commit bc4b1c7256
58 changed files with 7001 additions and 838 deletions

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -6,12 +6,12 @@
meteor-base@1.5.1 # Packages every Meteor app needs to have
mobile-experience@1.1.0 # Packages for a great mobile UX
mongo@1.14.6 # The database Meteor supports right now
mongo@1.15.0 # The database Meteor supports right now
jquery # Wrapper package for npm-installed jquery
reactive-var@1.0.11 # Reactive variable for tracker
tracker@1.2.0 # Meteor's client-side reactive programming library
standard-minifier-css@1.8.0 # CSS minifier run for production mode
standard-minifier-css@1.8.1 # CSS minifier run for production mode
standard-minifier-js@2.8.0 # JS minifier run for production mode
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
ecmascript@0.16.2 # Enable ECMAScript2015+ syntax in app code
@@ -22,7 +22,7 @@ svelte:compiler
#static-html@1.3.2
rdb:svelte-meteor-data
accounts-ui@1.4.2
accounts-password@2.3.0
accounts-password@2.3.1
svelte:blaze-integration
meteortesting:mocha
accounts-google@1.4.0
@@ -30,3 +30,6 @@ service-configuration@1.3.0
google-config-ui@1.0.3 # Adds the UI for logging in via Google
alanning:roles # Adds roles to the user
#zodern:melte
#babrahams:constellation # Alternative to MeteorToys - Has problems because it requires jquery 1.11.11 and everything else wants 2.x or 3.x
msavin:mongol # Free version of MeteorToys - Provides access to the client side MongoDB for debugging. (Ctrl-M to activate :: https://atmospherejs.com/msavin/mongol)

View File

@@ -1 +1 @@
METEOR@2.7
METEOR@2.7.2

View File

@@ -1,4 +1,4 @@
accounts-base@2.2.2
accounts-base@2.2.3
accounts-google@1.4.0
accounts-oauth@1.4.1
accounts-password@2.3.1
@@ -11,8 +11,8 @@ babel-compiler@7.9.0
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze@2.5.0
blaze-tools@1.1.2
blaze@2.6.0
blaze-tools@1.1.3
boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
@@ -37,14 +37,14 @@ geojson-utils@1.0.10
google-config-ui@1.0.3
google-oauth@1.4.2
hot-code-push@1.0.4
html-tools@1.1.2
html-tools@1.1.3
htmljs@1.1.1
http@1.4.4
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
jquery@3.0.0
launch-screen@1.3.0
less@3.0.2
less@4.0.0
localstorage@1.2.0
logging@1.3.1
meteor@1.10.0
@@ -52,27 +52,29 @@ meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
meteortoys:toykit@10.0.0
minifier-css@1.6.0
minifier-js@2.7.4
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modern-browsers@0.1.8
modules@0.18.0
modules-runtime@0.13.0
mongo@1.14.6
mongo-decimal@0.1.2
mongo@1.15.0
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
msavin:mongol@10.0.1
npm-mongo@4.3.1
oauth@2.1.2
oauth2@1.3.1
observe-sequence@1.0.19
observe-sequence@1.0.20
ordered-dict@1.1.0
promise@0.12.0
random@1.2.0
rate-limit@1.0.9
rdb:svelte-meteor-data@0.3.1
rdb:svelte-meteor-data@1.0.0
react-fast-refresh@0.2.3
reactive-dict@1.3.0
reactive-var@1.0.11
@@ -83,17 +85,18 @@ service-configuration@1.3.0
session@1.2.0
sha@1.0.9
shell-server@0.5.0
socket-stream-client@0.4.0
spacebars@1.2.0
spacebars-compiler@1.3.0
socket-stream-client@0.5.0
spacebars@1.3.0
spacebars-compiler@1.3.1
standard-minifier-css@1.8.1
standard-minifier-js@2.8.0
svelte:blaze-integration@0.4.0
svelte:compiler@3.46.4
templating@1.4.1
templating@1.4.2
templating-compiler@1.4.1
templating-runtime@1.5.0
templating-tools@1.2.1
templating-runtime@1.6.0
templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.5.4
underscore@1.0.10

3
client/app.css Normal file
View File

@@ -0,0 +1,3 @@
/*# sourceMappingURL=app.css.map */

1
client/app.css.map Normal file
View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":[],"names":[],"mappings":"","file":"app.css"}

108
client/app.sass Normal file
View File

@@ -0,0 +1,108 @@
/* CSS declarations go here */
.Mongol_row, .Mongol_row_name
color: white
*
box-sizing: border-box
html
padding: 0
margin: 0
body
font-family: sans-serif
background-attachment: fixed
position: absolute
top: 0
bottom: 0
left: 0
right: 0
padding: 0
margin: 0
//.container
// max-width: 600px
// margin: 0 auto
// min-height: 100%
// background: white
h1
font-size: 1.5em
margin: 0 0 1rem 0
display: inline-block
form
margin-top: 10px
margin-bottom: -10px
position: relative
.new-task input
box-sizing: border-box
padding: 10px 0
background: transparent
border: none
width: 100%
padding-right: 80px
font-size: 1em
.new-task input:focus
outline: 0
ul
margin: 0
padding: 0
background: white
.delete
float: right
font-weight: bold
background: none
font-size: 1em
border: none
position: relative
li
position: relative
list-style: none
padding: 15px
border-bottom: #eee solid 1px
li .text
margin-left: 10px
li.checked
color: #888
li.checked .text
text-decoration: line-through
li.private
background: #eee
border-color: #ddd
header .hide-completed
float: right
.toggle-private
margin-left: 5px
html
font-size: 16px
@media (max-width: 900px)
html
font-size: 14px
@media (max-width: 600px)
li
padding: 12px 15px
.search
width: 150px
clear: both
.new-task input
padding-bottom: 5px
html
font-size: 10px

View File

@@ -1,117 +1,4 @@
/* CSS declarations go here */
@import url(https://fonts.googleapis.com/css?family=Lato:400,300,300italic,400italic,700,700italic);
body {
padding: 10px;
font-family: sans-serif;
background-attachment: fixed;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 0;
margin: 0;
font-size: 14px;
}
header {
background: #d2edf4;
background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);
padding: 20px 15px 15px 15px;
position: relative;
}
#login-buttons {
display: block;
}
h1 {
font-size: 1.5em;
margin: 0 0 1rem 0;
display: inline-block;
}
form {
margin-top: 10px;
margin-bottom: -10px;
position: relative;
}
.new-task input {
box-sizing: border-box;
padding: 10px 0;
background: transparent;
border: none;
width: 100%;
padding-right: 80px;
font-size: 1em;
}
.new-task input:focus {
outline: 0;
}
ul {
margin: 0;
padding: 0;
background: white;
}
.delete {
float: right;
font-weight: bold;
background: none;
font-size: 1em;
border: none;
position: relative;
}
li {
position: relative;
list-style: none;
padding: 15px;
border-bottom: #eee solid 1px;
}
li .text {
margin-left: 10px;
}
li.checked {
color: #888;
}
li.checked .text {
text-decoration: line-through;
}
li.private {
background: #eee;
border-color: #ddd;
}
header .hide-completed {
float: right;
}
.toggle-private {
margin-left: 5px;
}
@media (max-width: 600px) {
li {
padding: 12px 15px;
}
.search {
width: 150px;
clear: both;
}
.new-task input {
padding-bottom: 5px;
}
}
html, body {
height: 100%;
width: 100%;
@@ -119,7 +6,6 @@ html, body {
padding: 0;
left: 0;
top: 0;
font-size: 100%;
}
/* ROOT FONT STYLES */
@@ -193,14 +79,13 @@ p {
/* ==== GRID SYSTEM ==== */
.container {
width: 90%;
width: 100%;
margin-left: auto;
margin-right: auto;
}
.row {
position: relative;
width: 100%;
}
.row [class^=col] {
float: left;
@@ -210,6 +95,7 @@ p {
.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12 {
width: 96%;
margin: auto;
}
.col-1-sm {
@@ -337,5 +223,131 @@ p {
max-width: 60rem;
}
}
/* CSS declarations go here */
.Mongol_row, .Mongol_row_name {
color: white;
}
* {
box-sizing: border-box;
}
html {
padding: 0;
margin: 0;
}
body {
font-family: sans-serif;
background-attachment: fixed;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 0;
margin: 0;
}
h1 {
font-size: 1.5em;
margin: 0 0 1rem 0;
display: inline-block;
}
form {
margin-top: 10px;
margin-bottom: -10px;
position: relative;
}
.new-task input {
box-sizing: border-box;
padding: 10px 0;
background: transparent;
border: none;
width: 100%;
padding-right: 80px;
font-size: 1em;
}
.new-task input:focus {
outline: 0;
}
ul {
margin: 0;
padding: 0;
background: white;
}
.delete {
float: right;
font-weight: bold;
background: none;
font-size: 1em;
border: none;
position: relative;
}
li {
position: relative;
list-style: none;
padding: 15px;
border-bottom: #eee solid 1px;
}
li .text {
margin-left: 10px;
}
li.checked {
color: #888;
}
li.checked .text {
text-decoration: line-through;
}
li.private {
background: #eee;
border-color: #ddd;
}
header .hide-completed {
float: right;
}
.toggle-private {
margin-left: 5px;
}
html {
font-size: 16px;
}
@media (max-width: 900px) {
html {
font-size: 14px;
}
}
@media (max-width: 600px) {
li {
padding: 12px 15px;
}
.search {
width: 150px;
clear: both;
}
.new-task input {
padding-bottom: 5px;
}
html {
font-size: 10px;
}
}
/*# sourceMappingURL=main.css.map */

View File

@@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["main.sass","simple-grid.sass"],"names":[],"mappings":"AAAA;ACAQ;ADCR;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAQD;EACC;EACA;EACA;EACA;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;EACA;EACA;;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;;;AAED;EACC;EACA;EACA;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;IACC;;;EAED;IACC;IACA;;;EAED;IACC;;;AChGF;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAED;AAEA;EACC;EACA;EACA;;;AAED;AAEA;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;AAEA;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;AAEA;EACC;EACA;EACA;;;AAED;EACC;EACA;;AAEA;EACC;EACA;EACA;;;AAEF;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;AACC;EAEA;IACC;;;AAEF;AACC;EAEA;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;AAEF;AACC;EAEA;IACC;IACA","file":"main.css"}
{"version":3,"sourceRoot":"","sources":["simple-grid.sass","app.sass"],"names":[],"mappings":"AAAQ;AAER;EACC;EACA;EACA;EACA;EACA;EACA;;;AAED;AAEA;EACC;EACA;EACA;;;AAED;AAEA;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;AAEA;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;AAEA;EAEC;EACA;EACA;;;AAED;EACC;;AAGA;EACC;EACA;EACA;;;AAEF;EACC;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;AACC;EAEA;IACC;;;AAEF;AACC;EAEA;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;AAEF;AACC;EAEA;IACC;IACA;;;ACzLF;AACA;EACC;;;AAED;EACC;;;AAED;EACC;EACA;;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAQD;EACC;EACA;EACA;;;AAED;EACC;EACA;EACA;;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;;;AAED;EACC;EACA;EACA;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;IACC;;;AAEF;EACC;IACC;;;EAED;IACC;IACA;;;EAED;IACC;;;EAED;IACC","file":"main.css"}

View File

@@ -1,5 +1,6 @@
<head>
<title>District Central</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

View File

@@ -1,101 +1,4 @@
/* CSS declarations go here */
body
padding: 10px
font-family: sans-serif
background-attachment: fixed
position: absolute
top: 0
bottom: 0
left: 0
right: 0
padding: 0
margin: 0
font-size: 14px
//.container
// max-width: 600px
// margin: 0 auto
// min-height: 100%
// background: white
header
background: #d2edf4
background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%)
padding: 20px 15px 15px 15px
position: relative
#login-buttons
display: block
h1
font-size: 1.5em
margin: 0 0 1rem 0
display: inline-block
form
margin-top: 10px
margin-bottom: -10px
position: relative
.new-task input
box-sizing: border-box
padding: 10px 0
background: transparent
border: none
width: 100%
padding-right: 80px
font-size: 1em
.new-task input:focus
outline: 0
ul
margin: 0
padding: 0
background: white
.delete
float: right
font-weight: bold
background: none
font-size: 1em
border: none
position: relative
li
position: relative
list-style: none
padding: 15px
border-bottom: #eee solid 1px
li .text
margin-left: 10px
li.checked
color: #888
li.checked .text
text-decoration: line-through
li.private
background: #eee
border-color: #ddd
header .hide-completed
float: right
.toggle-private
margin-left: 5px
@media (max-width: 600px)
li
padding: 12px 15px
.search
width: 150px
clear: both
.new-task input
padding-bottom: 5px
@import "./simple-grid.sass"
@import "./app.sass"

View File

@@ -1,228 +0,0 @@
@import url(https://fonts.googleapis.com/css?family=Lato:400,300,300italic,400italic,700,700italic);
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
left: 0;
top: 0;
font-size: 100%;
}
/* ROOT FONT STYLES */
* {
font-family: "Lato", Helvetica, sans-serif;
color: #333447;
line-height: 1.5;
}
/* TYPOGRAPHY */
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.375rem;
}
h4 {
font-size: 1.125rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.875rem;
}
p {
font-size: 1.125rem;
font-weight: 200;
line-height: 1.8;
}
.font-light {
font-weight: 300;
}
.font-regular {
font-weight: 400;
}
.font-heavy {
font-weight: 700;
}
/* POSITIONING */
.left {
text-align: left;
}
.right {
text-align: right;
}
.center {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.justify {
text-align: justify;
}
/* ==== GRID SYSTEM ==== */
.container {
width: 90%;
margin-left: auto;
margin-right: auto;
}
.row {
position: relative;
width: 100%;
}
.row [class^=col] {
float: left;
margin: 0.5rem 2%;
min-height: 0.125rem;
}
.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12 {
width: 96%;
}
.col-1-sm {
width: 4.33%;
}
.col-2-sm {
width: 12.66%;
}
.col-3-sm {
width: 21%;
}
.col-4-sm {
width: 29.33%;
}
.col-5-sm {
width: 37.66%;
}
.col-6-sm {
width: 46%;
}
.col-7-sm {
width: 54.33%;
}
.col-8-sm {
width: 62.66%;
}
.col-9-sm {
width: 71%;
}
.col-10-sm {
width: 79.33%;
}
.col-11-sm {
width: 87.66%;
}
.col-12-sm {
width: 96%;
}
.row::after {
content: "";
display: table;
clear: both;
}
.hidden-sm {
display: none;
}
@media only screen and (min-width: 33.75em) {
/* 540px */
.container {
width: 80%;
}
}
@media only screen and (min-width: 45em) {
/* 720px */
.col-1 {
width: 4.33%;
}
.col-2 {
width: 12.66%;
}
.col-3 {
width: 21%;
}
.col-4 {
width: 29.33%;
}
.col-5 {
width: 37.66%;
}
.col-6 {
width: 46%;
}
.col-7 {
width: 54.33%;
}
.col-8 {
width: 62.66%;
}
.col-9 {
width: 71%;
}
.col-10 {
width: 79.33%;
}
.col-11 {
width: 87.66%;
}
.col-12 {
width: 96%;
}
.hidden-sm {
display: block;
}
}
@media only screen and (min-width: 60em) {
/* 960px */
.container {
width: 75%;
max-width: 60rem;
}
}
/*# sourceMappingURL=simple-grid.css.map */

View File

@@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["simple-grid.sass"],"names":[],"mappings":"AAAQ;AAER;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAED;AAEA;EACC;EACA;EACA;;;AAED;AAEA;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;AAEA;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;AAEA;EACC;EACA;EACA;;;AAED;EACC;EACA;;AAEA;EACC;EACA;EACA;;;AAEF;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;EACA;EACA;;;AAED;EACC;;;AAED;AACC;EAEA;IACC;;;AAEF;AACC;EAEA;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;EAED;IACC;;;AAEF;AACC;EAEA;IACC;IACA","file":"simple-grid.css"}

View File

@@ -7,7 +7,6 @@ html, body
padding: 0
left: 0
top: 0
font-size: 100%
/* ROOT FONT STYLES
@@ -69,13 +68,14 @@ p
/* ==== GRID SYSTEM ====
.container
width: 90%
//width: 90%
width: 100%
margin-left: auto
margin-right: auto
.row
position: relative
width: 100%
//width: 100%
[class^="col"]
float: left
@@ -84,6 +84,7 @@ p
.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12
width: 96%
margin: auto
.col-1-sm
width: 4.33%

56
imports/api/admin.js Normal file
View File

@@ -0,0 +1,56 @@
import {Meteor} from "meteor/meteor";
import { _ } from 'underscore';
import { Roles } from 'meteor/alanning:roles';
if (Meteor.isServer) {
Meteor.methods({
'admin.fixRecords'(input) {
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
console.log("In Fix Records");
console.log("Deleting invalid records...");
// Delete any records missing key fields.
Meteor.Records.remove({serial: {$exists: false}});
Meteor.Records.remove({deviceId: {$exists: false}});
Meteor.Records.remove({endTime: {$exists: false}});
console.log("Consolidating records...");
let emails = _.uniq(Meteor.Records.find({}, {
sort: {email: 1},
fields: {email: true}
}).fetch().map(function (x) {
return x.email;
}), true);
emails.forEach(email => {
// Find all records for the user sorted from oldest to newest.
let records = Meteor.Records.find({email}, {sort: {startTime: 1}}).fetch();
let newRecords = [];
let record = records[0];
for (let index = 1; index < records.length; index++) {
let nextRecord = records[index];
if (record.deviceId === nextRecord.deviceId) {
record.endTime = nextRecord.endTime;
record.count += nextRecord.count;
record.internalCount += nextRecord.internalCount;
} else {
if (!record.endTime) record.endTime = nextRecord.startTime;
newRecords.push(record);
record = nextRecord;
}
}
newRecords.push(record);
Meteor.Records.remove({email});
for (let index = 0; index < newRecords.length; index++) {
Meteor.Records.insert(newRecords[index]);
}
});
}
},
});
}

View File

@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import { MongoClient } from 'mongodb';
//import {Roles} from 'alanning/roles';
//export const Records = new Mongo.Collection('records');
let client;
@@ -9,21 +10,59 @@ let database;
let dataCollection;
if (Meteor.isServer) {
let uri = process.env.MONGO_URL2;
// let uri = process.env.MONGO_URL2;
// uri = "mongodb://localhost:27017";
//
// //client = new MongoClient(uri);
// MongoClient.connect(uri, (err, c) => {
// client = c;
// database = client.db("avusd-data-collection");
// dataCollection = database.collection("records");
// dataCollection.find({deviceId: "1e3e99ef-adf4-4aa2-8784-205bc60f0ce3"}).toArray((e, a) => {
// if(e) console.log(e);
// else console.log("Found " + a.length + " records.");
// })
// });
client = new MongoClient(uri);
database = client.db("avusd-data-collection");
dataCollection = database.collection("records");
// This code only runs on the server
Meteor.publish('chromebookData', function(deviceId) {
check(deviceId, String);
return dataCollection.find({deviceId});
});
// let results = Meteor.Records.find({deviceId: "1e3e99ef-adf4-4aa2-8784-205bc60f0ce3"}).fetch();
// console.log(results);
}
if (Meteor.isServer) {
Meteor.methods({
/**
* Collects Chromebook history given one of the possible parameters.
* @param params An object with a single attribute. The attribute must be one of: deviceId, serial, email. It will find all Chromebook data that starts with the given attribute value.
* @returns {any} Array of Chromebook data objects.
*/
'DataCollection.chromebookData'(params) {
if(Roles.userIsInRole(Meteor.userId(), "laptop-management", {anyScope:true})) {
let query = {};
if (params.deviceId) query.deviceId = params.regex ? {
$regex: params.deviceId,
$options: "i"
} : params.deviceId;
else if (params.serial) query.serial = params.regex ? {
$regex: params.serial,
$options: "i"
} : params.serial;
else if (params.email) query.email = params.regex ? {
$regex: params.email,
$options: "i"
} : params.email;
// console.log("Collecting Chromebook Data: ");
// console.log(query);
let result = Meteor.Records.find(query).fetch();
// console.log("Found: ");
// console.log(result);
return result;
}
else {return null;}
}
// 'tasks.setChecked'(taskId, setChecked) {
// check(taskId, String);
// check(setChecked, Boolean);
@@ -50,3 +89,4 @@ Meteor.methods({
// Tasks.update(taskId, { $set: { private: setToPrivate } });
// },
});
}

View File

@@ -1,2 +1,3 @@
import "./records.js";
import "./users.js";
import "./data-collection.js";
import "./admin.js";

View File

@@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Roles } from 'meteor/alanning:roles';
import { check } from 'meteor/check';
if (Meteor.isServer) {
Meteor.publish(null, function() {
@@ -9,14 +10,59 @@ if (Meteor.isServer) {
else {
this.ready();
}
})
});
// Meteor.methods({
// 'users.setupInitialRoles'() {
// Roles.createRole('admin');
// Roles.createRole('laptop-management');
// Roles.addRolesToParent('laptop-management', 'admin');
// Roles.addUsersToRoles("zwbMiaSKHix4bWQ8d", 'admin', 'global');
// }
// });
Meteor.publish(null, function() {
return Meteor.roles.find({});
});
Meteor.publish("allUsers", function() {
// console.log(Meteor.isServer);
// console.log("AllUsers");
// console.log("Meteor.userId(): " + Meteor.userId());
// // console.log(Roles.userIsInRole(Meteor.userId(), "laptop-management"));
// console.log(Meteor.roleAssignment.find({ 'user._id': Meteor.userId() }).fetch());
// console.log(Roles.userIsInRole(Meteor.user(), "admin", {anyScope:true}));
// Note: For some reason the {anyScope: true} is necessary on the server for the function to actually check roles.
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
//console.log(Meteor.users.find({}).fetch());
return Meteor.users.find({});
}
else {
return [];
}
});
Meteor.publish("allRoleAssignments", function() {
// Note: For some reason the {anyScope: true} is necessary on the server for the function to actually check roles.
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
return Meteor.roleAssignment.find({});
}
else {
return [];
}
});
Meteor.methods({
'users.setUserRoles'(userId, roles) {
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
check(userId, String);
check(roles, Array);
Roles.setUserRoles(userId, roles, {anyScope: true});
}
},
// 'tasks.setPrivate'(taskId, setToPrivate) {
// check(taskId, String);
// check(setToPrivate, Boolean);
//
// const task = Tasks.findOne(taskId);
//
// // Make sure only the task owner can make a task private
// if (task.owner !== this.userId) {
// throw new Meteor.Error('not-authorized');
// }
//
// Tasks.update(taskId, { $set: { private: setToPrivate } });
// },
});
}

View File

@@ -1,13 +1,70 @@
import { Accounts } from 'meteor/accounts-base'
import { Roles } from 'meteor/alanning:roles'
import {Meteor} from "meteor/meteor";
if(Meteor.isCLient) {
if(Meteor.isClient) {
Accounts.ui.config({
passwordSignupFields: 'USERNAME_ONLY'
});
}
Accounts.config({
// Allow only certain email domains.
restrictCreationByEmailDomain: function(address) {
return new RegExp('.*@avpanthers.org$', 'i').test(address)
let pattern = process.env.EMAIL_REGEX;
return new RegExp(pattern, 'i').test(address)
}
});
if(Meteor.isServer) {
let adminEmail = process.env.ADMIN_EMAIL;
let watchForAdmin = false;
//Setup the roles.
Roles.createRole('admin', {unlessExists: true});
Roles.createRole('laptop-management', {unlessExists: true});
Roles.addRolesToParent('laptop-management', 'admin', {unlessExists: true});
//Roles.addUsersToRoles("zwbMiaSKHix4bWQ8d", 'admin', 'global', {unlessExists: true});
// If we are passed an email address that should be admin by default, then ensure that user is admin, or mark it as needing to be admin if the user ever logs in.
// Given that this app requires Google OAuth2, and we expect logins to be restricted to district email addresses, this should be very secure.
if(adminEmail) {
let user = Meteor.users.findOne({"services.google.email": adminEmail});
if(user) {
let assignment = Meteor.roleAssignment.findOne({'user._id': user._id, "role._id": "admin"});
// console.log("Admin Role Assignment: " + JSON.stringify(assignment));
if(!assignment) {
Roles.addUsersToRoles(user._id, ['admin']);
}
}
else {
watchForAdmin = true;
}
}
// Listen for users logging in so we can setup the admin user automatically once they log in the first time.
if(watchForAdmin) {
// TODO: It would be nice to remove this handler after the admin user is found, but the docs are pretty ambiguous about how to do that. Not a big deal, just annoying.
Accounts.onLogin(function (data) {
// console.log("User logged in:");
// console.log(data.user.services.google.email);
// data.user == Meteor.user()
//console.log(JSON.stringify(Meteor.user()));
if (watchForAdmin) {
try {
if (data.user.services.google.email === adminEmail) {
Roles.addUsersToRoles(data.user._id, ['admin']);
watchForAdmin = false;
}
} catch (err) {
console.log(err);
}
}
});
}
}

8
imports/ui/Admin.svelte Normal file
View File

@@ -0,0 +1,8 @@
<script>
import {Meteor} from "meteor/meteor";
const fixRecords = () => {Meteor.call("admin.fixRecords");}
</script>
<div class="container">
<button type="button" on:click={fixRecords}>Fix Records</button>
</div>

View File

@@ -0,0 +1,26 @@
<!-- Announcer.svelte - From the tinro docs: https://github.com/AlexxNB/tinro -->
<!-- This will be included in the App.svelte so that URL changes are announced in a way that accessibility tools can read the page. -->
<script>
import { router } from 'tinro';
$: current = $router.path === '/' ? 'Home' : $router.path.slice(1);
</script>
<div aria-live="assertive" aria-atomic="true">
{#key current}
Navigated to {current}
{/key}
</div>
<style>
div {
position: absolute;
left: 0;
top: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
width: 1px;
height: 1px;
}
</style>

View File

@@ -1,179 +1,39 @@
<style>
nav {
font-size: 2rem;
}
a {
display: block;
color: green;
text-decoration: none;
}
html {
background-color: #000121;
font-family: 'Roboto', sans-serif;
}
/** Forbidden CSS */
.maincontainer {
position: relative;
top: -50px;
transform: scale(0.8);
background: url("/public/images/forbidden/HauntedHouseBackground.png");
background-repeat: no-repeat;
background-position: center;
background-size: 700px 600px;
width: 800px;
height: 600px;
margin: 0px auto;
display: grid;
}
.foregroundimg {
position: relative;
width: 100%;
top: -230px;
z-index: 5;
}
.errorcode {
position: relative;
top: -200px;
font-family: 'Creepster', cursive;
color: white;
text-align: center;
font-size: 6em;
letter-spacing: 0.1em;
}
.errortext {
position: relative;
top: -260px;
color: #FBD130;
text-align: center;
text-transform: uppercase;
font-size: 1.8em;
}
.bat {
opacity: 0;
position: relative;
transform-origin: center;
z-index: 3;
}
.bat:nth-child(1) {
top: 380px;
left: 120px;
transform: scale(0.5);
animation: 13s 1s flyBat1 infinite linear;
}
.bat:nth-child(2) {
top: 280px;
left: 80px;
transform: scale(0.3);
animation: 8s 4s flyBat2 infinite linear;
}
.bat:nth-child(3) {
top: 200px;
left: 150px;
transform: scale(0.4);
animation: 12s 2s flyBat3 infinite linear;
}
.body {
position: relative;
width: 50px;
top: 12px;
}
.wing {
width: 150px;
position: relative;
transform-origin: right center;
}
.leftwing {
left: 30px;
animation: 0.8s flapLeft infinite ease-in-out;
}
.rightwing {
left: -180px;
transform: scaleX(-1);
animation: 0.8s flapRight infinite ease-in-out;
}
@keyframes flapLeft {
0% { transform: rotateZ(0); }
50% { transform: rotateZ(10deg) rotateY(40deg); }
100% { transform: rotateZ(0); }
}
@keyframes flapRight {
0% { transform: scaleX(-1) rotateZ(0); }
50% { transform: scaleX(-1) rotateZ(10deg) rotateY(40deg); }
100% { transform: scaleX(-1) rotateZ(0); }
}
@keyframes flyBat1 {
0% { opacity: 1; transform: scale(0.5)}
25% { opacity: 1; transform: scale(0.5) translate(-400px, -330px) }
50% { opacity: 1; transform: scale(0.5) translate(400px, -800px) }
75% { opacity: 1; transform: scale(0.5) translate(600px, 100px) }
100% { opacity: 1; transform: scale(0.5) translate(100px, 300px) }
}
@keyframes flyBat2 {
0% { opacity: 1; transform: scale(0.3)}
25% { opacity: 1; transform: scale(0.3) translate(200px, -330px) }
50% { opacity: 1; transform: scale(0.3) translate(-300px, -800px) }
75% { opacity: 1; transform: scale(0.3) translate(-400px, 100px) }
100% { opacity: 1; transform: scale(0.3) translate(100px, 300px) }
}
@keyframes flyBat3 {
0% { opacity: 1; transform: scale(0.4)}
25% { opacity: 1; transform: scale(0.4) translate(-350px, -330px) }
50% { opacity: 1; transform: scale(0.4) translate(400px, -800px) }
75% { opacity: 1; transform: scale(0.4) translate(-600px, 100px) }
100% { opacity: 1; transform: scale(0.4) translate(100px, 300px) }
}
/*@media only screen and (max-width: 850px) {
.maincontainer {
transform: scale(0.6);
width: 600px;
height: 400px;
background-size: 600px 400px;
}
.errortext {
font-size: 1em;
}
}*/
</style>
<script>
import {Meteor} from "meteor/meteor";
import {Route} from 'tinro';
import {Route, router} from 'tinro';
import {onMount} from 'svelte';
import {useTracker} from 'meteor/rdb:svelte-meteor-data';
import {Roles} from 'meteor/alanning:roles';
import Chromebooks from './Chromebooks.svelte';
import Users from './Users.svelte';
import TestTable from './TestTable.svelte';
import ListUsers from './ListUsers.svelte';
import Admin from './Admin.svelte';
import Announcer from './Announcer.svelte';
import {BlazeTemplate} from 'meteor/svelte:blaze-integration';
import {Records} from '../api/records.js'
import ServiceConfiguration from "meteor/service-configuration";
//import './imports/ui/App.css';
// When the URL changes, run the code... in this case to scroll to the top.
router.subscribe(_ => window.scrollTo(0, 0));
//let currentUser;
onMount(async () => {
// Meteor.subscribe('records');
});
// onMount(async () => {
// // Meteor.subscribe('records');
// });
// $: incompleteCount = useTracker(() => Tasks.find({checked: {$ne: true}}).count());
$: currentUser = useTracker(() => Meteor.user());
$: isAdmin = useTracker(() => Roles.userIsInRole(currentUser._id, 'laptop-management', 'global'));
$: canManageLaptops = false;
$: isAdmin = false;
Tracker.autorun(() => {
// For some reason currentUser is always null here, and is not reactive (user changes and this does not get re-called).
let user = Meteor.user();
canManageLaptops = user && Roles.userIsInRole(user._id, 'laptop-management', 'global');
isAdmin = user && Roles.userIsInRole(user._id, 'admin', 'global');
});
// Tracker.autorun(() => {
// let user = Meteor.user();
// let isManagement = user ? Roles.userIsInRole(user._id, 'laptop-management', 'global') : 0;
//
// if(user && isManagement) {
// new ProcessLaptops({
// target: document.getElementById('appView')
// });
// }
// });
// const taskStore = Tasks.find({}, {sort: {createdAt: -1}});
// $: {
@@ -194,8 +54,7 @@
Meteor.loginWithGoogle({loginStyle: "popup", requestOfflineToken: true}, (err) => {
if (err) {
console.log(err);
}
else {
} else {
//console.log("Logged in");
}
})
@@ -207,63 +66,177 @@
</script>
<Route path="/">
<Announcer/>
<div class="container">
<header class="row">
<div class="col-12 logoContainer">
<img class="logo" src="/images/logo.svg"/>
<div class="login">
{#if !$currentUser}
<button type="button" role="button" on:click={performLogin}>Login</button>
{:else}
<button type="button" role="button" on:click={performLogout}>Logout</button>
{/if}
</div>
</div>
<div class="col-12 center" style="margin-bottom: 0"><h1 style="margin-bottom: 0">District Central</h1></div>
<div class="col-12 center">
<div class="nav-separator"></div>
</div>
<nav class="col-12 center">
<h1>AVUSD District Central</h1>
<a href="/">Home</a>
{#if isAdmin}
{#if canManageLaptops}
<a href="/chromebooks">Chromebooks</a>
{/if}
</nav>
{#if !$currentUser}
<button type="button" on:click={performLogin}>Login</button>
{:else}
<button type="button" on:click={performLogout}>Logout</button>
{#if canManageLaptops}
<a href="/users">Users</a>
{/if}
{#if isAdmin}
<a href="/admin">Admin</a>
{/if}
<!-- <a href="/TestTable">Test</a>-->
<!-- <a href="/ListUsers">List Users</a>-->
</nav>
</header>
</div>
</Route>
{#if $currentUser}
{#if isAdmin}
<Route path="/chromebooks/*">
<Chromebooks/>
</Route>
{:else}
<Route fallback redirect="/"/>
{/if}
{:else}
<div className="container" style="background-color: #4d4242">
<div class="maincontainer row">
<div class="bat">
<img class="wing leftwing"
src="/images/forbidden/bat-wing.png">
<img class="body"
src="/images/forbidden/bat-body.png" alt="bat">
<img class="wing rightwing"
src="/images/forbidden/bat-wing.png">
</div>
<div class="bat">
<img class="wing leftwing"
src="/images/forbidden/bat-wing.png">
<img class="body"
src="/images/forbidden/bat-body.png" alt="bat">
<img class="wing rightwing"
src="/images/forbidden/bat-wing.png">
</div>
<div class="bat">
<img class="wing leftwing"
src="/images/forbidden/bat-wing.png">
<img class="body"
src="/images/forbidden/bat-body.png" alt="bat">
<img class="wing rightwing"
src="/images/forbidden/bat-wing.png">
</div>
<img class="foregroundimg" src="/images/forbidden/HauntedHouseForeground.png" alt="haunted house">
<Route path="/">
<div class="container">
<div class="row">
TODO: Some statistics and such.
</div>
<h1 class="errorcode">ERROR 403</h1>
<div class="errortext">This area is forbidden. Turn back now!</div>
</div>
</Route>
<Route path="/ListUsers">
<!-- <ListUsers/>-->
</Route>
<Route path="/admin">
{#if isAdmin}
<Admin/>
{/if}
</Route>
<Route path="/TestTable/*">
<!-- <TestTable/>-->
</Route>
<Route path="/chromebooks/*">
{#if canManageLaptops}
<Chromebooks/>
{:else}
<!-- User not authorized to use this UI. Don't render anything because it is likely the user is still loading and will have access in a moment. -->
{/if}
</Route>
<Route path="/users/*">
{#if isAdmin}
<Users/>
{:else}
<!-- User not authorized to use this UI. Don't render anything because it is likely the user is still loading and will have access in a moment. -->
{/if}
</Route>
<style>
@font-face {
font-family: 'KaushanScript-Regular';
font-style: normal;
font-weight: 400;
src: url('/fonts/KaushanScript-Regular.ttf') format('truetype');
}
.nav-separator {
height: 0.2rem;
width: 40%;
margin-left: 30%;
background-color: white;
}
nav {
font-size: 1.4rem;
}
nav a {
display: inline-block;
color: #a6a6ea;
text-decoration: none;
}
nav a:hover {
color: #6363ee;
text-decoration: underline;
}
nav a + a {
margin-left: 2rem;
}
html {
background-color: #000121;
font-family: 'Roboto', sans-serif;
}
header {
background: #2c031c;
color: white;
/*background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);*/
padding: 20px 15px 15px 15px;
position: relative;
}
h1 {
color: white;
font-family: "KaushanScript-Regular", sans-serif;
font-size: 4rem;
}
.logo {
position: absolute;
left: 0;
top: 0;
width: 8rem;
}
.logoContainer {
height: 0;
position: relative;
}
.login {
position: absolute;
right: 0;
top: 0;
}
.login button {
background: #7171ec;
border-radius: 999px;
box-shadow: #5E5DF0 0 10px 20px -10px;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
font-family: Inter, Helvetica, "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols, -apple-system, system-ui, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", sans-serif;
font-weight: 700;
line-height: 24px;
opacity: 1;
outline: 0 solid transparent;
padding: 8px 18px;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
width: fit-content;
word-break: break-word;
border: 0;
}
@media (max-width: 600px) {
.logo {
position: absolute;
left: 0;
right: 0;
top: 0;
margin: 0 auto;
width: 8rem;
}
.logoContainer {
height: 6rem;
}
}
</style>

View File

@@ -4,38 +4,100 @@
}
</style>
<script>
import {router} from 'tinro';
import {Html5QrcodeScanner} from "html5-qrcode";
import {Html5Qrcode} from "html5-qrcode";
import {useTracker} from "meteor/rdb:svelte-meteor-data";
import {Meteor} from "meteor/meteor";
import { Session } from 'meteor/session';
function onScanSuccess(decodedText, decodedResult) {
console.log('Code matched ' + decodedResult);
document.getElementById("log").prepend(decodedText);
let c = 0;
$: deviceId = null;
$: chromebookData = [];
$: {
if (deviceId) {
Meteor.call("DataCollection.chromebookData", deviceId, (error, result) => {
// console.log("Call returned");
if (error) {
console.error(error);
} else {
chromebookData = result;
// console.log("result: " + result);
}
});
}
else {
chromebookData = [];
}
}
function onScanFailure(error) {
function fakeScan() {
setTimeout(() => {
let decodedText = "1e3e99ef-adf4-4aa2-8784-205bc60f0ce3";
//deviceId = decodedText;
//window.location.href="/chromebooks/byDevice/" + encodeURIComponent(decodedText);
//router.location.goto("/chromebooks/byDevice/" + encodeURIComponent(decodedText));
router.goto("/chromebooks?deviceId=" + encodeURIComponent(decodedText));
}, 1000);
}
function clear() {
deviceId = "";
}
function setDeviceId(id) {
deviceId = id;
}
let html5QrCode;
function scanner() {
let html5QrcodeScanner = new Html5QrcodeScanner("reader", {
fps: 10,
qrbox: {width: 250, height: 250}
}, /* verbose */ false);
html5QrCode = new Html5Qrcode("reader");
const config = {fps: 10, qrbox: {width: 250, height: 250}};
html5QrCode.start({facingMode: "environment"}, config, (decodedText, decodedResult) => {
//console.log('Code matched ' + decodedText);
//document.getElementById("log").prepend(JSON.stringify(decodedResult));
//setDeviceId(decodedText);
window.location.href="/chromebooks?deviceId=" + encodeURIComponent(decodedText);
html5QrcodeScanner.render(onScanSuccess, onScanFailure);
// Stop Scanning
html5QrCode.stop().then((ignore) => {
// QR Code scanning is stopped.
}).catch((err) => {
// Stop failed, handle it.
});
}, (error) => {
//TODO:
});
}
</script>
<div className="container">
<div class="row">
Hello World!
<div class="container">
<div class="row col-12" style="margin-bottom: 1rem">
<button type="button" on:click={fakeScan}>Fake Scan</button>
<button type="button" on:click={clear}>Clear</button>
</div>
<div class="row">
<div use:scanner id="reader" width="300px"></div>
<!-- <div class="row">-->
<!-- <div use:scanner id="reader" width="300px"></div>-->
<!-- </div>-->
<div class="row col-12">
<a href="#" on:click={scanner}>Scan</a>
<!-- <button type="button" on:click={scanner}>Scan</button>-->
<div id="reader" width="250px"></div>
</div>
<div class="row">
<br/>
<div class="row col-12">
<div id="log" class="col-12">
</div>
</div>
<div class="row col-12">
<ul>
{#each chromebookData as data}
<li>{data.email}<br/>{data.serial}<br/>{new Date(data.startTime).toLocaleDateString("en-US") + "-" + new Date(data.endTime).toLocaleDateString("en-US")}</li>
{/each}
</ul>
</div>
</div>

View File

@@ -1,32 +1,187 @@
<style>
nav {
font-size: 2rem;
}
a {
display: block;
color: green;
a.button {
background: #7171ec;
border-radius: 999px;
box-shadow: #5E5DF0 0 10px 20px -10px;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
font-family: Inter,Helvetica,"Apple Color Emoji","Segoe UI Emoji",NotoColorEmoji,"Noto Color Emoji","Segoe UI Symbol","Android Emoji",EmojiSymbols,-apple-system,system-ui,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 24px;
opacity: 1;
outline: 0 solid transparent;
padding: 8px 18px;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
width: fit-content;
word-break: break-word;
border: 0;
text-decoration: none;
}
input {
--background: #fff;
--border-default: #D0D0DF;
--border-active: #3D6DF9;
/*--shadow-default: #{rgba(#202048, .12)};*/
/*--shadow-active: #{rgba(#3D6DF9, .25)};*/
--text-color: #818190;
--placeholder-color: #C9C9D9;
--placeholder-color-hover: #BABAC9;
--close: #818190;
--close-light: #BABAC9;
--close-background: #F1F1FA;
width: 100%;
max-width: 240px;
border-radius: 8px;
box-shadow: inset 0 0 0 1px;
}
.options {
margin: 0 auto;
max-width: 800px;
}
</style>
<script>
import {Route} from 'tinro';
import {Route, router, meta} from 'tinro';
import ChromebookScan from './ChromebookScan.svelte';
import {Meteor} from "meteor/meteor";
$: serialInput = null;
$: emailInput = null;
function serialSearch() {
router.goto("/chromebooks?serial=" + encodeURIComponent(serialInput) + "&regex=true");
}
function emailSearch() {
router.goto("/chromebooks?email=" + encodeURIComponent(emailInput) + "&regex=true");
}
// console.log("Loading Script");
// //Attempt to listen for URL changes (query portion specifically).
// (function(history){
// const pushState = history.pushState;
// history.pushState = function(state) {
// if (typeof history.onpushstate == "function") {
// history.onpushstate({state: state});
// }
// // Call your custom function here
// console.log("Push state");
// console.log(history);
// console.log(arguments);
// return pushState.apply(history, arguments);
// }
// })(window.history);
// const params = Object.fromEntries(new URLSearchParams(window.location.search));
//
// console.log("Params: ");
// console.log(params);
$: deviceId = null;
$: serial = null;
$: email = null
$: regex = false;
$: router.subscribe(query => {
deviceId = router.location.query.get("deviceId");
serial = router.location.query.get("serial");
email = router.location.query.get("email");
regex = router.location.query.get("regex");
if(deviceId) deviceId = decodeURIComponent(deviceId);
if(serial) serial = decodeURIComponent(serial);
if(email) email = decodeURIComponent(email);
if(regex) regex = true;
// console.log("Query:");
// console.log(deviceId);
// console.log(serial);
// console.log(email);
});
$: chromebookData = null;
$: {
if(deviceId || serial || email) {
let params = {};
if(deviceId) params.deviceId = deviceId;
else if(serial) params.serial = serial;
else if(email) params.email = email;
if(regex) params.regex = true;
Meteor.call("DataCollection.chromebookData", params, (error, result) => {
if (error) {
console.error(error);
} else {
chromebookData = result;
}
});
}
else {
chromebookData = null;
}
}
</script>
<Route path="/">
<div className="container">
<div class="row">
<nav class="col-12 center">
<Route path="/" let:meta>
{#if chromebookData}
<div class="container">
<div class="row col-12">
<ul>
{#each chromebookData as data}
<li><a href="/chromebooks?email={encodeURIComponent(data.email)}">{data.email}</a><br/>
<a href="/chromebooks?deviceId={encodeURIComponent(data.deviceId)}">{data.deviceId}</a><br/>
<a href="/chromebooks?serial={encodeURIComponent(data.serial)}">{data.serial}</a><br/>
{new Date(data.startTime).toLocaleDateString("en-US") + "-" + new Date(data.endTime).toLocaleDateString("en-US")}
</li>
{/each}
</ul>
</div>
</div>
{:else}
<div class="container">
<div class="row col-12">
<div class="options">
<h1>Chromebook Management</h1>
<a href="chromebooks/scan">Scan A Chromebook</a>
<a href="chromebooks/byStudent">Chromebook History By Student</a>
<a href="chromebooks/byChromebook">Chromebook History By Chromebook</a>
</nav>
<ul>
<li>
By Chromebook Device ID: <a href="/chromebooks/scan" className="button">Scan</a>
</li>
<li>
By Chromebook Serial Number: <br/>
<input type="text" bind:value="{serialInput}" placeholder="Serial Number"/><br/>
<button type="button" role="button" on:click={serialSearch}>Search</button>
</li>
<li>
By Email Address: <br/>
<input type="text" bind:value="{emailInput}" placeholder="Email"/>@avpanthers.org<br/>
<button type="button" role="button" on:click={emailSearch}>Search</button>
</li>
</ul>
</div>
</div>
</div>
{/if}
</Route>
<Route path="/byDevice/:deviceId">
<div class="container">
<div class="row col-12">
<ul>
{#each chromebookData as data}
<li>{data.email}<br/>{data.serial}
<br/>{new Date(data.startTime).toLocaleDateString("en-US") + "-" + new Date(data.endTime).toLocaleDateString("en-US")}
</li>
{/each}
</ul>
</div>
</div>
</Route>
<Route path="/scan">
<ChromebookScan/>
</Route>

181
imports/ui/FlexTable.svelte Normal file
View File

@@ -0,0 +1,181 @@
<script>
export let rows;
export let columns;
export let rowKey;
export let edited;
// Setup a width for each column.
columns.forEach(column => {
let min = column.minWidth ? Math.max(10, column.minWidth) : 10;
let weight = column.weight ? Math.max(1, column.weight) : 1;
column.width = 'minmax(' + min + 'px, ' + weight + 'fr)';
});
let gridTemplateColumns = columns.map(({width}) => width).join(' ');
let headerBeingResized = null;
let horizontalScrollOffset = 0;
const initResize = ({target}) => {
headerBeingResized = target.parentNode;
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', completeResize);
headerBeingResized.classList.add('header--being-resized');
};
const completeResize = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', completeResize);
headerBeingResized.classList.remove('header--being-resized');
headerBeingResized = null;
};
const onMouseMove = e => {
try {
// Calculate the desired width.
horizontalScrollOffset = document.documentElement.scrollLeft;
let parentX = Math.round(headerBeingResized.getBoundingClientRect().x);
const width = horizontalScrollOffset + (e.clientX - parentX);
// Update the column object with the new size value.
const column = columns.find(({element}) => element === headerBeingResized);
column.width = Math.max(column.minWidth, width) + "px";
// Ensure all the column widths are converted to fixed sizes.
columns.forEach((column, index) => {
if((index < columns.length - 1) && (column.width.startsWith('minmax'))) {
column.width = parseInt(column.element.clientWidth, 10) + 'px';
}
});
// Render the new column sizes.
gridTemplateColumns = columns.map(({width}) => width).join(' ');
} catch(e) {console.log(e);}
}
let selectedRowElement = null;
const selectRow = (e, row) => {
let element = e.target;
while(element && element.nodeName !== "TR") element = element.parentNode;
if(selectedRowElement) {
selectedRowElement.classList.remove('selected');
}
selectedRowElement = element;
element.classList.add('selected');
}
let editorContainer;
const editRow = (e, row) => {
let element = e.target;
while(element && element.nodeName !== "TR") element = element.parentNode;
let editor = element.querySelector('.editor');
// Save the edited row so the editor has access to it.
$edited = row;
if(editor) {
editor.appendChild(editorContainer);
}
editorContainer.classList.remove('hidden');
}
</script>
<div bind:this={editorContainer} class="hidden"><slot>Slot</slot></div>
<table style="--grid-template-columns: {gridTemplateColumns}">
<thead>
<tr>
{#each columns as column}
<th bind:this={column.element}>{column.title} <span class="resize-handle" on:mousedown={initResize}></span></th>
{/each}
</tr>
</thead>
<tbody>
{#each $rows as row (rowKey(row))}
<!-- data-key="{rowKey(row)}"-->
<tr class:hidden={row === $edited} on:mousedown={(e) => selectRow(e, row)} on:dblclick={(e) => editRow(e, row)}>
{#each columns as column}
<td>{column.value(row)}</td>
{/each}
<td class="editor"></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => {$edited = null}} type="button">Stop Editing</button>
<style>
table {
width: auto;
-webkit-box-flex: 1;
flex: 1;
display: grid;
border-collapse: collapse;
grid-template-columns: var(--grid-template-columns);
}
thead, tbody, tr {
display: contents;
}
th, td {
padding: 15px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
th {
position: -webkit-sticky;
position: sticky;
top: 0;
background: #5cb85c;
text-align: left;
font-weight: normal;
font-size: 1.1rem;
color: white;
position: relative;
}
th:last-child {
border: 0;
}
.resize-handle {
position: absolute;
top: 0;
right: 0;
bottom: 0;
background: black;
opacity: 0;
width: 3px;
cursor: col-resize;
}
th:last-child .resize-handle {
display: none;
}
.resize-handle:hover, .header--being-resized .resize-handle {
opacity: 0.5;
}
th:hover .resize-handle {
opacity: 0.3;
}
td {
padding-top: 10px;
padding-bottom: 10px;
color: #808080;
}
tr:nth-child(even) {
background: #f8f6ff;
}
:global(.selected), :global(.selected) > td {
background-color: yellow;
}
.editor {
grid-column: 1 / 4;
display: none;
}
/*:global(td.hidden) {*/
/* display: none;*/
/*}*/
:global(tr.hidden) > td:not(.editor) {
display: none !important;
}
:global(tr.hidden) > td.editor {
display: block !important;
}
:global(div.hidden) {
display: none !important;
}
</style>

View File

@@ -0,0 +1,50 @@
<script>
import {Meteor} from "meteor/meteor";
import TestUsers from "./TestUsers.svelte";
import {writable} from "svelte/store";
$: users = Meteor.users.find({});
const columns = [
{
key: "_id",
title: "ID",
value: v => v._id,
minWidth: 20,
weight: 1,
cls: "id",
}, {
key: "name",
title: "Name",
value: v => v.profile.name,
minWidth: 100,
weight: 1,
cls: "name",
}, {
key: "roles",
title: "Roles",
value: user => {
return Roles.getRolesForUser(user, {anyScope: true});
},
minWidth: 150,
weight: 2,
cls: "roles",
}
];
const getRowKey = user => {return user._id;}
let edited = writable(null);
</script>
{#await Meteor.subscribe('allUsers')}
Loading...
{:then allUsers}
<TestUsers bind:rows="{users}" columns="{columns}" rowKey="{getRowKey}" bind:edited={edited}>
{#if $edited}
<input type="text" bind:value={$edited.profile.name}/>
{/if}
</TestUsers>
{:catch error}
{error.message}
{/await}

View File

@@ -0,0 +1,158 @@
<style>
.maincontainer {
position: relative;
top: -50px;
transform: scale(0.8);
background: url("/public/images/forbidden/HauntedHouseBackground.png");
background-repeat: no-repeat;
background-position: center;
background-size: 700px 600px;
width: 800px;
height: 600px;
margin: 0px auto;
display: grid;
}
.foregroundimg {
position: relative;
width: 100%;
top: -230px;
z-index: 5;
}
.errorcode {
position: relative;
top: -200px;
font-family: 'Creepster', cursive;
color: white;
text-align: center;
font-size: 6em;
letter-spacing: 0.1em;
}
.errortext {
position: relative;
top: -260px;
color: #FBD130;
text-align: center;
text-transform: uppercase;
font-size: 1.8em;
}
.bat {
opacity: 0;
position: relative;
transform-origin: center;
z-index: 3;
}
.bat:nth-child(1) {
top: 380px;
left: 120px;
transform: scale(0.5);
animation: 13s 1s flyBat1 infinite linear;
}
.bat:nth-child(2) {
top: 280px;
left: 80px;
transform: scale(0.3);
animation: 8s 4s flyBat2 infinite linear;
}
.bat:nth-child(3) {
top: 200px;
left: 150px;
transform: scale(0.4);
animation: 12s 2s flyBat3 infinite linear;
}
.body {
position: relative;
width: 50px;
top: 12px;
}
.wing {
width: 150px;
position: relative;
transform-origin: right center;
}
.leftwing {
left: 30px;
animation: 0.8s flapLeft infinite ease-in-out;
}
.rightwing {
left: -180px;
transform: scaleX(-1);
animation: 0.8s flapRight infinite ease-in-out;
}
@keyframes flapLeft {
0% { transform: rotateZ(0); }
50% { transform: rotateZ(10deg) rotateY(40deg); }
100% { transform: rotateZ(0); }
}
@keyframes flapRight {
0% { transform: scaleX(-1) rotateZ(0); }
50% { transform: scaleX(-1) rotateZ(10deg) rotateY(40deg); }
100% { transform: scaleX(-1) rotateZ(0); }
}
@keyframes flyBat1 {
0% { opacity: 1; transform: scale(0.5)}
25% { opacity: 1; transform: scale(0.5) translate(-400px, -330px) }
50% { opacity: 1; transform: scale(0.5) translate(400px, -800px) }
75% { opacity: 1; transform: scale(0.5) translate(600px, 100px) }
100% { opacity: 1; transform: scale(0.5) translate(100px, 300px) }
}
@keyframes flyBat2 {
0% { opacity: 1; transform: scale(0.3)}
25% { opacity: 1; transform: scale(0.3) translate(200px, -330px) }
50% { opacity: 1; transform: scale(0.3) translate(-300px, -800px) }
75% { opacity: 1; transform: scale(0.3) translate(-400px, 100px) }
100% { opacity: 1; transform: scale(0.3) translate(100px, 300px) }
}
@keyframes flyBat3 {
0% { opacity: 1; transform: scale(0.4)}
25% { opacity: 1; transform: scale(0.4) translate(-350px, -330px) }
50% { opacity: 1; transform: scale(0.4) translate(400px, -800px) }
75% { opacity: 1; transform: scale(0.4) translate(-600px, 100px) }
100% { opacity: 1; transform: scale(0.4) translate(100px, 300px) }
}
/*@media only screen and (max-width: 850px) {
.maincontainer {
transform: scale(0.6);
width: 600px;
height: 400px;
background-size: 600px 400px;
}
.errortext {
font-size: 1em;
}
}*/
</style>
<div className="container" style="background-color: #4d4242">
<div class="maincontainer row">
<div class="bat">
<img class="wing leftwing"
src="/images/forbidden/bat-wing.png">
<img class="body"
src="/images/forbidden/bat-body.png" alt="bat">
<img class="wing rightwing"
src="/images/forbidden/bat-wing.png">
</div>
<div class="bat">
<img class="wing leftwing"
src="/images/forbidden/bat-wing.png">
<img class="body"
src="/images/forbidden/bat-body.png" alt="bat">
<img class="wing rightwing"
src="/images/forbidden/bat-wing.png">
</div>
<div class="bat">
<img class="wing leftwing"
src="/images/forbidden/bat-wing.png">
<img class="body"
src="/images/forbidden/bat-body.png" alt="bat">
<img class="wing rightwing"
src="/images/forbidden/bat-wing.png">
</div>
<img class="foregroundimg" src="/images/forbidden/HauntedHouseForeground.png" alt="haunted house">
</div>
<h1 class="errorcode">ERROR 403</h1>
<div class="errortext">This area is forbidden. Turn back now!</div>
</div>

105
imports/ui/Table.svelte Normal file
View File

@@ -0,0 +1,105 @@
<script>
import { createEventDispatcher } from "svelte";
import { onMount } from "svelte";
/** @type {Array<Object>} */
export let columns;
/** @type {Array<Object>} */
export let rows;
const dispatch = createEventDispatcher();
// Create a UUID for creating unique instance styles later.
const instanceId = Date.now().toString(36) + Math.random().toString(16).slice(2);
let columnByKey;
$: {
columnByKey = {};
columns.forEach(column => {
columnByKey[column.key] = column;
});
}
let columnClasses = [];
// Create a custom class (style) for each column so we can control the column sizes.
$: {
// Remove old classes.
if(columnClasses && columnClasses.length) {
columnClasses.forEach(cls => {
try {
document.getElementsByTagName('head')[0].removeChild(cls);
}
catch(e) {console.log(e);}
});
}
// Create a unique class for each column so we can manage column sizes.
columns.forEach((column, index) => {
try {
let cls = document.createElement('style');
cls.type = 'text/css';
column.customClassName = 'svelte-' + instanceId + '-column-' + index;
cls.innerHTML = column.customClassName + "{min-width: " + column.width + "; max-width: " + column.width + "; width: " + column.width + ";}";
columnClasses[index] = cls;
document.getElementsByTagName('head')[0].appendChild(cls);
} catch(e) {console.log(e);}
});
}
// Used to create a list of classes for tags.
const asStringArray = v => [].concat(v).filter(v => typeof v === "string" && v !== "").join(" ");
let section;
let table;
// onMount(async () => {
// //let hiddenHeaders = table.querySelectorAll("tbody tr:first-child td");
// let hiddenHeaders = table.querySelectorAll("th");
// section.querySelectorAll("th").forEach((th, index) => {
// hiddenHeaders[index].style.width = th.getBoundingClientRect().width + 'px';
// });
// });
</script>
<section bind:this={section}>
<thead>
<tr>
{#each columns as column}
<th class="{column.cls} cell" style="--width-{column.key}: {column.width}; min-width: var(--width-{column.key}); max-width: var(--width-{column.key}); width: var(--width-{column.key});">{column.title}</th>
{/each}
</tr>
</thead>
</section>
<table bind:this={table}>
<thead class="table-head">
<tr class="table-head">
{#each columns as column}
<th class="{column.cls} cell table-head" style="--width-{column.key}: {column.width}; min-width: var(--width-{column.key}); max-width: var(--width-{column.key}); width: var(--width-{column.key});" data-key="{column.key}">{column.title}</th>
{/each}
</tr>
</thead>
<tbody>
{#each rows as row}
<tr>
{#each columns as column}
<td class="{column.cls} cell" style="--width-{column.key}: {column.width}; min-width: var(--width-{column.key}); max-width: var(--width-{column.key}); width: var(--width-{column.key});">{column.value(row)}</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<style>
section th:not(:last-child) {
border-right: 4px solid gray;
}
.table-head {
visibility: hidden;
line-height: 0;
margin: 0;
padding: 0;
}
.cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

335
imports/ui/Table2.svelte Normal file
View File

@@ -0,0 +1,335 @@
<script>
import { createEventDispatcher } from "svelte";
/** @type {Array<Object>} */
export let columns;
/** @type {Array<Object>} */
export let rows;
/** @type {Array<Object>} */
export let c_rows;
/** @type {Array<number>} */
export let sortOrders = [1, -1];
// READ AND WRITE
/** @type {string} */
export let sortBy = "";
/** @type {number} */
export let sortOrder = sortOrders?.[0] || 1;
/** @type {Object} */
export let filterSelections = {};
// expand
/** @type {Array.<string|number>} */
export let expanded = [];
// READ ONLY
/** @type {string} */
export let expandRowKey = null;
/** @type {string} */
export let expandSingle = false;
/** @type {string} */
export let iconAsc = "▲";
/** @type {string} */
export let iconDesc = "▼";
/** @type {string} */
export let iconSortable = "";
/** @type {string} */
export let iconExpand = "▼";
/** @type {string} */
export let iconExpanded = "▲";
/** @type {boolean} */
export let showExpandIcon = false;
/** @type {string} */
export let classNameTable = "";
/** @type {string} */
export let classNameThead = "";
/** @type {string} */
export let classNameTbody = "";
/** @type {string} */
export let classNameSelect = "";
/** @type {string} */
export let classNameInput = "";
/** @type {string} */
export let classNameRow = "";
/** @type {string} */
export let classNameCell = "";
/** @type {string} class added to the expanded row*/
export let classNameRowExpanded = "";
/** @type {string} class added to the expanded row*/
export let classNameExpandedContent = "";
/** @type {string} class added to the cell that allows expanding/closing */
export let classNameCellExpand = "";
const dispatch = createEventDispatcher();
let sortFunction = () => "";
// Validation
if (!Array.isArray(expanded)) throw "'expanded' needs to be an array";
let showFilterHeader = columns.some(c => {
// check if there are any filter or search headers
return c.filterOptions !== undefined || c.searchValue !== undefined;
});
let filterValues = {};
let columnByKey;
$: {
columnByKey = {};
columns.forEach(col => {
columnByKey[col.key] = col;
});
}
$: colspan = (showExpandIcon ? 1 : 0) + columns.length;
console.log(rows);
$: c_rows = rows
.filter(r => {
// get search and filter results/matches
return Object.keys(filterSelections).every(f => {
// check search (text input) matches
let resSearch =
filterSelections[f] === "" ||
(columnByKey[f].searchValue &&
(columnByKey[f].searchValue(r) + "")
.toLocaleLowerCase()
.indexOf((filterSelections[f] + "").toLocaleLowerCase()) >= 0);
// check filter (dropdown) matches
let resFilter =
resSearch ||
filterSelections[f] === undefined ||
// default to value() if filterValue() not provided in col
filterSelections[f] ===
(typeof columnByKey[f].filterValue === "function"
? columnByKey[f].filterValue(r)
: columnByKey[f].value(r));
return resFilter;
});
})
.map(r =>
Object.assign({}, r, {
// internal row property for sort order
$sortOn: sortFunction(r),
// internal row property for expanded rows
$expanded:
expandRowKey !== null && expanded.indexOf(r[expandRowKey]) >= 0,
})
)
.sort((a, b) => {
if (!sortBy) return 0;
else if (a.$sortOn > b.$sortOn) return sortOrder;
else if (a.$sortOn < b.$sortOn) return -sortOrder;
return 0;
});
const asStringArray = v =>
[]
.concat(v)
.filter(v => typeof v === "string" && v !== "")
.join(" ");
const calculateFilterValues = () => {
filterValues = {};
columns.forEach(c => {
if (typeof c.filterOptions === "function") {
filterValues[c.key] = c.filterOptions(rows);
} else if (Array.isArray(c.filterOptions)) {
// if array of strings is provided, use it for name and value
filterValues[c.key] = c.filterOptions.map(val => ({
name: val,
value: val,
}));
}
});
};
$: {
let col = columnByKey[sortBy];
if (
col !== undefined &&
col.sortable === true &&
typeof col.value === "function"
) {
sortFunction = r => col.value(r);
}
}
$: {
// if filters are enabled, watch rows and columns
if (showFilterHeader && columns && rows) {
calculateFilterValues();
}
}
const updateSortOrder = colKey => {
return colKey === sortBy
? sortOrders[
(sortOrders.findIndex(o => o === sortOrder) + 1) % sortOrders.length
]
: sortOrders[0];
};
const handleClickCol = (event, col) => {
if (col.sortable) {
sortOrder = updateSortOrder(col.key);
sortBy = sortOrder ? col.key : undefined;
}
dispatch("clickCol", { event, col, key: col.key });
};
const handleClickRow = (event, row) => {
dispatch("clickRow", { event, row });
};
const handleClickExpand = (event, row) => {
row.$expanded = !row.$expanded;
const keyVal = row[expandRowKey];
if (expandSingle && row.$expanded) {
expanded = [keyVal];
} else if (expandSingle) {
expanded = [];
} else if (!row.$expanded) {
expanded = expanded.filter(r => r != keyVal);
} else {
expanded = [...expanded, keyVal];
}
dispatch("clickExpand", { event, row });
};
const handleClickCell = (event, row, key) => {
dispatch("clickCell", { event, row, key });
};
</script>
<table class={asStringArray(classNameTable)}>
<thead class={asStringArray(classNameThead)}>
{#if showFilterHeader}
<tr>
{#each columns as col}
<th class={asStringArray([col.headerFilterClass])}>
{#if col.searchValue !== undefined}
<input
bind:value={filterSelections[col.key]}
class={asStringArray(classNameInput)}
/>
{:else if filterValues[col.key] !== undefined}
<select
bind:value={filterSelections[col.key]}
class={asStringArray(classNameSelect)}
>
<option value={undefined} />
{#each filterValues[col.key] as option}
<option value={option.value}>{option.name}</option>
{/each}
</select>
{/if}
</th>
{/each}
{#if showExpandIcon}
<th />
{/if}
</tr>
{/if}
<slot name="header" {sortOrder} {sortBy}>
<tr>
{#each columns as col}
<th
on:click={e => handleClickCol(e, col)}
class={asStringArray([
col.sortable ? "isSortable" : "",
col.headerClass,
])}
>
{col.title}
{#if sortBy === col.key}
{@html sortOrder === 1 ? iconAsc : iconDesc}
{:else if col.sortable}
{@html iconSortable}
{/if}
</th>
{/each}
{#if showExpandIcon}
<th />
{/if}
</tr>
</slot>
</thead>
<tbody class={asStringArray(classNameTbody)}>
{#each c_rows as row, n}
<slot name="row" {row} {n}>
<tr on:click={e => { handleClickRow(e, row); }} class={asStringArray([classNameRow, row.$expanded && classNameRowExpanded])}>
{#each columns as col}
<td on:click={e => {handleClickCell(e, row, col.key);}} class={asStringArray([col.class, classNameCell])}>
{#if col.renderComponent}
<svelte:component
this={col.renderComponent.component || col.renderComponent}
{...col.renderComponent.props || {}}
{row}
{col}
/>
{:else}
{@html col.renderValue ? col.renderValue(row) : col.value(row)}
{/if}
</td>
{/each}
{#if showExpandIcon}
<td on:click={e => handleClickExpand(e, row)} class={asStringArray(["isClickable", classNameCellExpand])}>
{@html row.$expanded ? iconExpand : iconExpanded}
</td>
{/if}
</tr>
{#if row.$expanded}
<tr class={asStringArray(classNameExpandedContent)}>
<td {colspan}>
<slot name="expanded" {row} {n} />
</td>
</tr>
{/if}
</slot>
{/each}
</tbody>
</table>
<style>
table {
width: 100%;
}
.isSortable {
cursor: pointer;
}
.isClickable {
cursor: pointer;
}
tr th select {
width: 100%;
}
</style>

View File

@@ -0,0 +1,66 @@
<script>
import {Route, router, meta} from 'tinro';
import {Meteor} from "meteor/meteor";
import FlexTable from "./FlexTable.svelte";
import {useTracker} from "meteor/rdb:svelte-meteor-data";
import {writable} from "svelte/store";
const columns = [
{
key: "_id",
title: "ID",
value: v => v._id,
minWidth: 20,
weight: 1,
}, {
key: "text",
title: "Text",
value: v => v.text,
minWidth: 100,
weight: 1,
}
];
let rows = writable([
{
_id: "1",
text: "A"
},
{
_id: "2",
text: "B"
},
{
_id: "3",
text: "C"
},
{
_id: "4",
text: "D"
},
]);
const getRowKey = (row) => row._id;
let edited = writable(null);
let text = "";
const addRow = () => {
$rows[$rows.length] = {_id: "" + ($rows.length + 1), text};
text = "";
}
</script>
<Route path="/">
<div class="container">
<div class="row col-12 table">
<FlexTable columns="{columns}" bind:rows="{$rows}" rowKey="{getRowKey}" edited="{edited}">
My Editor....
</FlexTable>
</div>
</div>
<div class="container">
<div class="row col-12">
<input type="text" bind:value={text}/>
<button type="button" on:click={addRow}>Add</button>
</div>
</div>
</Route>

181
imports/ui/TestUsers.svelte Normal file
View File

@@ -0,0 +1,181 @@
<script>
export let rows;
export let columns;
export let rowKey;
export let edited;
// Setup a width for each column.
columns.forEach(column => {
let min = column.minWidth ? Math.max(10, column.minWidth) : 10;
let weight = column.weight ? Math.max(1, column.weight) : 1;
column.width = 'minmax(' + min + 'px, ' + weight + 'fr)';
});
let gridTemplateColumns = columns.map(({width}) => width).join(' ');
let headerBeingResized = null;
let horizontalScrollOffset = 0;
const initResize = ({target}) => {
headerBeingResized = target.parentNode;
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', completeResize);
headerBeingResized.classList.add('header--being-resized');
};
const completeResize = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', completeResize);
headerBeingResized.classList.remove('header--being-resized');
headerBeingResized = null;
};
const onMouseMove = e => {
try {
// Calculate the desired width.
horizontalScrollOffset = document.documentElement.scrollLeft;
let parentX = Math.round(headerBeingResized.getBoundingClientRect().x);
const width = horizontalScrollOffset + (e.clientX - parentX);
// Update the column object with the new size value.
const column = columns.find(({element}) => element === headerBeingResized);
column.width = Math.max(column.minWidth, width) + "px";
// Ensure all the column widths are converted to fixed sizes.
columns.forEach((column, index) => {
if((index < columns.length - 1) && (column.width.startsWith('minmax'))) {
column.width = parseInt(column.element.clientWidth, 10) + 'px';
}
});
// Render the new column sizes.
gridTemplateColumns = columns.map(({width}) => width).join(' ');
} catch(e) {console.log(e);}
}
let selectedRowElement = null;
const selectRow = (e, row) => {
let element = e.target;
while(element && element.nodeName !== "TR") element = element.parentNode;
if(selectedRowElement) {
selectedRowElement.classList.remove('selected');
}
selectedRowElement = element;
element.classList.add('selected');
}
let editorContainer;
const editRow = (e, row) => {
let element = e.target;
while(element && element.nodeName !== "TR") element = element.parentNode;
let editor = element.querySelector('.editor');
// Save the edited row so the editor has access to it.
$edited = row;
if(editor) {
editor.appendChild(editorContainer);
}
editorContainer.classList.remove('hidden');
}
</script>
<div bind:this={editorContainer} class="hidden"><slot>Slot</slot></div>
<table style="--grid-template-columns: {gridTemplateColumns}">
<thead>
<tr>
{#each columns as column}
<th bind:this={column.element}>{column.title} <span class="resize-handle" on:mousedown={initResize}></span></th>
{/each}
</tr>
</thead>
<tbody>
{#each $rows as row (rowKey(row))}
<!-- data-key="{rowKey(row)}"-->
<tr class:hidden={row === $edited} on:mousedown={(e) => selectRow(e, row)} on:dblclick={(e) => editRow(e, row)}>
{#each columns as column}
<td>{column.value(row)}</td>
{/each}
<td class="editor"></td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => {$edited = null}} type="button">Stop Editing</button>
<style>
table {
width: auto;
-webkit-box-flex: 1;
flex: 1;
display: grid;
border-collapse: collapse;
grid-template-columns: var(--grid-template-columns);
}
thead, tbody, tr {
display: contents;
}
th, td {
padding: 15px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
th {
position: -webkit-sticky;
position: sticky;
top: 0;
background: #5cb85c;
text-align: left;
font-weight: normal;
font-size: 1.1rem;
color: white;
position: relative;
}
th:last-child {
border: 0;
}
.resize-handle {
position: absolute;
top: 0;
right: 0;
bottom: 0;
background: black;
opacity: 0;
width: 3px;
cursor: col-resize;
}
th:last-child .resize-handle {
display: none;
}
.resize-handle:hover, .header--being-resized .resize-handle {
opacity: 0.5;
}
th:hover .resize-handle {
opacity: 0.3;
}
td {
padding-top: 10px;
padding-bottom: 10px;
color: #808080;
}
tr:nth-child(even) {
background: #f8f6ff;
}
:global(.selected), :global(.selected) > td {
background-color: yellow;
}
.editor {
grid-column: 1 / 4;
display: none;
}
/*:global(td.hidden) {*/
/* display: none;*/
/*}*/
:global(tr.hidden) > td:not(.editor) {
display: none !important;
}
:global(tr.hidden) > td.editor {
display: block !important;
}
:global(div.hidden) {
display: none !important;
}
</style>

191
imports/ui/Users.svelte Normal file
View File

@@ -0,0 +1,191 @@
<script>
import {Route, router, meta} from 'tinro';
import {Meteor} from "meteor/meteor";
import FlexTable from "./FlexTable.svelte";
import {useTracker} from "meteor/rdb:svelte-meteor-data";
import {writable} from "svelte/store";
const columns = [
{
key: "_id",
title: "ID",
value: v => v._id,
minWidth: 20,
weight: 1,
cls: "id",
}, {
key: "name",
title: "Name",
value: v => v.profile.name,
minWidth: 100,
weight: 1,
cls: "name",
}, {
key: "roles",
title: "Roles",
value: user => {
return Roles.getRolesForUser(user, {anyScope: true});
},
minWidth: 150,
weight: 2,
cls: "roles",
}
];
const getRowKey = user => {return user._id;}
function changeColWidth() {
columns[0].width = '200px';
}
const editRow = (row) => {
//TODO: Setup the editor for the given row.
}
let edited = writable(null);
let editedPermissions = null;
$: rows = Meteor.users.find({});
edited.subscribe((value) => {
if(value) {
editedPermissions = {
isAdmin: Roles.userIsInRole(value, "admin", {anyScope: true}),
laptopManagement: Roles.userIsInRole(value, "laptop-management", {anyScope: true}),
}
}
});
const applyChanges = () => {
let roles = [];
if(editedPermissions.isAdmin) {
roles.push('admin');
}
else {
if(editedPermissions.laptopManagement) {
roles.push('laptop-management');
}
}
Meteor.call("users.setUserRoles", $edited._id, roles);
edited.set(null);
}
const rejectChanges = () => {
edited.set(null);
}
</script>
<Route path="/" let:meta>
<div class="container">
<div class="row col-12 table">
{#await Promise.all([Meteor.subscribe('allUsers'), Meteor.subscribe('allRoleAssignments')])}
Loading...
{:then allUsers}
<FlexTable bind:rows={rows} columns="{columns}" rowKey="{getRowKey}" bind:edited="{edited}">
{#if editedPermissions}
<div class="editorContainer">
<label style="grid-column: 1/4; font-weight: 800; border-bottom: 2px solid #888; margin-bottom: 0.5rem">{$edited.profile.name}</label>
<label class="checkbox" style="grid-column: 1/4;"><input type="checkbox" bind:checked="{editedPermissions.isAdmin}" style="--form-control-color: black"/> Administrator</label>
<div class="insetPermissions">
<label class="checkbox"><input type="checkbox" disabled="{editedPermissions.isAdmin}" bind:checked="{editedPermissions.laptopManagement}" style="--form-control-color: black"/> Laptop Management</label>
</div>
<button type="button" style="grid-column: 2/2;" class="button accept-button" on:click={applyChanges}> </button>
<button type="button" style="grid-column: 3/3;" class="button reject-button" on:click={rejectChanges}> </button>
</div>
{/if}
</FlexTable>
<button type="button" on:click="{changeColWidth}">Change Width</button>
{:catch error}
{error.message}
{/await}
</div>
</div>
</Route>
<style>
:root {
--form-control-disabled: #959495;
}
.checkbox {
display: grid;
grid-template-columns: 1em auto;
gap: 0.5em;
margin: 0;
padding: 0;
align-items: center;
}
input[type="checkbox"] {
/* Add if not using autoprefixer */
-webkit-appearance: none;
appearance: none;
/* For iOS < 15 to remove gradient background */
background-color: #fff;
/* Not removed via appearance */
margin: 0;
font: inherit;
color: currentColor;
width: 1.15em;
height: 1.15em;
border: 0.15em solid currentColor;
border-radius: 0.15em;
/*transform: translateY(-0.075em);*/
display: grid;
place-content: center;
padding: 0;
}
input[type="checkbox"]::before {
content: "";
width: 0.65em;
height: 0.65em;
transform: scale(0);
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em var(--form-control-color);
/* Windows High Contrast Mode */
background-color: CanvasText;
/* Make it a check mark shape. */
transform-origin: bottom left;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
input[type="checkbox"]:focus {
outline: max(2px, 0.15em) solid currentColor;
outline-offset: max(2px, 0.15em);
}
input[type="checkbox"]:disabled {
--form-control-color: var(--form-control-disabled);
color: var(--form-control-disabled);
cursor: not-allowed;
}
.editorContainer {
display: grid;
grid-template-columns: auto 2em 2em;
}
.insetPermissions {
margin-top: 0.5rem;
margin-left: 1.5rem;
display: grid;
grid-column: 1/4;
}
button.button {
border: none;
padding: 6px;
margin: 0;
cursor: pointer;
-webkit-appearance: button;
}
button.accept-button {
width: 2rem;
height: 2rem;
background-color: black;
clip-path: polygon(11% 69%, 31% 93%, 94% 8%, 73% 6%, 33% 62%, 12% 40%);
}
button.reject-button {
width: 2rem;
height: 2rem;
background-color: black;
clip-path: polygon(18% 90%, 37% 89%, 50% 64%, 62% 89%, 81% 88%, 59% 44%, 82% 13%, 62% 12%, 49% 35%, 28% 12%, 11% 12%, 37% 43%);
}
</style>

109
imports/ui/temp.svelte Normal file
View File

@@ -0,0 +1,109 @@
<script>
import {Meteor} from "meteor/meteor";
import {Route, router} from 'tinro';
import {useTracker} from 'meteor/rdb:svelte-meteor-data';
import {Roles} from 'meteor/alanning:roles';
import Chromebooks from './Chromebooks.svelte';
import Users from './Users.svelte';
import ListUsers from './ListUsers.svelte';
import Admin from './Admin.svelte';
import Announcer from './Announcer.svelte';
// When the URL changes, run the code... in this case to scroll to the top.
router.subscribe(_ => window.scrollTo(0, 0));
$: currentUser = useTracker(() => Meteor.user());
$: canManageLaptops = false;
$: isAdmin = false;
Tracker.autorun(() => {
// For some reason currentUser is always null here, and is not reactive (user changes and this does not get re-called).
let user = Meteor.user();
canManageLaptops = user && Roles.userIsInRole(user._id, 'laptop-management', 'global');
isAdmin = user && Roles.userIsInRole(user._id, 'admin', 'global');
});
function performLogin() {
//Login style can be "popup" or "redirect". I am not sure we need to request and offline token.
Meteor.loginWithGoogle({loginStyle: "popup", requestOfflineToken: true}, (err) => {
if (err) {
console.log(err);
} else {
//console.log("Logged in");
}
})
}
function performLogout() {
Meteor.logout();
}
</script>
<Announcer/>
<div class="container">
<header class="row">
<div class="col-12 logoContainer">
<img class="logo" src="/images/logo.svg"/>
<div class="login">
{#if !$currentUser}
<button type="button" role="button" on:click={performLogin}>Login</button>
{:else}
<button type="button" role="button" on:click={performLogout}>Logout</button>
{/if}
</div>
</div>
<div class="col-12 center" style="margin-bottom: 0"><h1 style="margin-bottom: 0">District Central</h1></div>
<div class="col-12 center">
<div class="nav-separator"></div>
</div>
<nav class="col-12 center">
<a href="/">Home</a>
{#if canManageLaptops}
<a href="/chromebooks">Chromebooks</a>
{/if}
{#if canManageLaptops}
<a href="/users">Users</a>
{/if}
{#if isAdmin}
<a href="/admin">Admin</a>
{/if}
<!-- <a href="/TestTable">Test</a>-->
<!-- <a href="/ListUsers">List Users</a>-->
</nav>
</header>
</div>
<Route path="/">
<div class="container">
<div class="row">
TODO: Some statistics and such.
</div>
</div>
</Route>
<Route path="/ListUsers">
<ListUsers/>
</Route>
<Route path="/admin">
{#if isAdmin}
<Admin/>
{/if}
</Route>
<Route path="/chromebooks/*">
{#if canManageLaptops}
<Chromebooks/>
{:else}
<!-- User not authorized to use this UI. Don't render anything because it is likely the user is still loading and will have access in a moment. -->
{/if}
</Route>
<Route path="/users/*">
{#if isAdmin}
<Users/>
{:else}
<!-- User not authorized to use this UI. Don't render anything because it is likely the user is still loading and will have access in a moment. -->
{/if}
</Route>
<style>
...
</style>

528
package-lock.json generated
View File

@@ -11,9 +11,13 @@
"html5-qrcode": "^2.2.0",
"jquery": "^3.6.0",
"meteor-node-stubs": "^1.0.0",
"moment": "^2.29.2",
"mongodb": "^4.4.1",
"svelte": "^3.46.4",
"tinro": "^0.6.12",
"underscore": "^1.13.2",
"winston": "^3.7.2",
"winston-daily-rotate-file": "^4.6.1",
"ws": "^8.4.2"
},
"devDependencies": {
@@ -32,6 +36,24 @@
"node": ">=6.9.0"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@rollup/pluginutils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.0.tgz",
@@ -73,6 +95,11 @@
"node": "*"
}
},
"node_modules/async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -152,6 +179,46 @@
"node": "*"
}
},
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/color-string": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz",
"integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"dependencies": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"node_modules/connect-route": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/connect-route/-/connect-route-0.1.5.tgz",
@@ -180,12 +247,35 @@
"node": ">=0.10"
}
},
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"node_modules/fecha": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz",
"integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q=="
},
"node_modules/file-stream-rotator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
"integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
"dependencies": {
"moment": "^2.29.1"
}
},
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"node_modules/get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
@@ -219,16 +309,54 @@
}
]
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"node_modules/logform": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz",
"integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==",
"dependencies": {
"@colors/colors": "1.5.0",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
}
},
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
@@ -919,6 +1047,14 @@
"node": ">=0.4"
}
},
"node_modules/moment": {
"version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
"engines": {
"node": "*"
}
},
"node_modules/mongodb": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.4.1.tgz",
@@ -945,6 +1081,27 @@
"whatwg-url": "^11.0.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
@@ -974,6 +1131,19 @@
"node": ">=6"
}
},
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
@@ -994,6 +1164,33 @@
"rollup": "1 || 2"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safe-stable-stringify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==",
"engines": {
"node": ">=10"
}
},
"node_modules/saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
@@ -1006,6 +1203,14 @@
"node": ">=6"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -1037,6 +1242,22 @@
"memory-pager": "^1.0.2"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
"engines": {
"node": "*"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/svelte": {
"version": "3.46.4",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.46.4.tgz",
@@ -1045,6 +1266,11 @@
"node": ">= 8"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"node_modules/tinro": {
"version": "0.6.12",
"resolved": "https://registry.npmjs.org/tinro/-/tinro-0.6.12.tgz",
@@ -1061,6 +1287,11 @@
"node": ">=12"
}
},
"node_modules/triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@@ -1070,6 +1301,16 @@
"node": ">=4"
}
},
"node_modules/underscore": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz",
"integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g=="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -1090,6 +1331,56 @@
"node": ">=12"
}
},
"node_modules/winston": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.7.2.tgz",
"integrity": "sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng==",
"dependencies": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.4.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.5.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-daily-rotate-file": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.6.1.tgz",
"integrity": "sha512-Ycch4LZmTycbhgiI2eQXBKI1pKcEQgAqmBjyq7/dC6Dk77nasdxvhLKraqTdCw7wNDSs8/M0jXaLATHquG7xYg==",
"dependencies": {
"file-stream-rotator": "^0.6.1",
"object-hash": "^2.0.1",
"triple-beam": "^1.3.0",
"winston-transport": "^4.4.0"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"winston": "^3"
}
},
"node_modules/winston-transport": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
"dependencies": {
"logform": "^2.3.2",
"readable-stream": "^3.6.0",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 6.4.0"
}
},
"node_modules/ws": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",
@@ -1120,6 +1411,21 @@
"regenerator-runtime": "^0.13.4"
}
},
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="
},
"@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"requires": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"@rollup/pluginutils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.0.tgz",
@@ -1155,6 +1461,11 @@
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1197,6 +1508,46 @@
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"requires": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-string": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz",
"integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==",
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"requires": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"connect-route": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/connect-route/-/connect-route-0.1.5.tgz",
@@ -1216,12 +1567,35 @@
"resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz",
"integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ=="
},
"enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"fecha": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz",
"integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q=="
},
"file-stream-rotator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
"integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
"requires": {
"moment": "^2.29.1"
}
},
"fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
@@ -1238,16 +1612,48 @@
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
},
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"logform": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz",
"integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==",
"requires": {
"@colors/colors": "1.5.0",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
}
},
"memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
@@ -1815,6 +2221,11 @@
}
}
},
"moment": {
"version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
},
"mongodb": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.4.1.tgz",
@@ -1836,6 +2247,24 @@
"whatwg-url": "^11.0.0"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
},
"one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"requires": {
"fn.name": "1.x.x"
}
},
"pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
@@ -1853,6 +2282,16 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
@@ -1867,6 +2306,16 @@
"@rollup/pluginutils": "4"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safe-stable-stringify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg=="
},
"saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
@@ -1876,6 +2325,14 @@
"sparse-bitfield": "^3.0.3"
}
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"requires": {
"is-arrayish": "^0.3.1"
}
},
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -1899,11 +2356,29 @@
"memory-pager": "^1.0.2"
}
},
"stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
}
},
"svelte": {
"version": "3.46.4",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.46.4.tgz",
"integrity": "sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg=="
},
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"tinro": {
"version": "0.6.12",
"resolved": "https://registry.npmjs.org/tinro/-/tinro-0.6.12.tgz",
@@ -1917,12 +2392,27 @@
"punycode": "^2.1.1"
}
},
"triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
},
"underscore": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz",
"integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -1937,6 +2427,44 @@
"webidl-conversions": "^7.0.0"
}
},
"winston": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.7.2.tgz",
"integrity": "sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng==",
"requires": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.4.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.5.0"
}
},
"winston-daily-rotate-file": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.6.1.tgz",
"integrity": "sha512-Ycch4LZmTycbhgiI2eQXBKI1pKcEQgAqmBjyq7/dC6Dk77nasdxvhLKraqTdCw7wNDSs8/M0jXaLATHquG7xYg==",
"requires": {
"file-stream-rotator": "^0.6.1",
"object-hash": "^2.0.1",
"triple-beam": "^1.3.0",
"winston-transport": "^4.4.0"
}
},
"winston-transport": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
"requires": {
"logform": "^2.3.2",
"readable-stream": "^3.6.0",
"triple-beam": "^1.3.0"
}
},
"ws": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz",

View File

@@ -14,9 +14,13 @@
"html5-qrcode": "^2.2.0",
"jquery": "^3.6.0",
"meteor-node-stubs": "^1.0.0",
"moment": "^2.29.2",
"mongodb": "^4.4.1",
"svelte": "^3.46.4",
"tinro": "^0.6.12",
"underscore": "^1.13.2",
"winston": "^3.7.2",
"winston-daily-rotate-file": "^4.6.1",
"ws": "^8.4.2"
},
"meteor": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3922
public/images/logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 398 KiB

13
server/DataCollection.js Normal file
View File

@@ -0,0 +1,13 @@
import {MongoInternals} from 'meteor/mongo';
import {Meteor} from 'meteor/meteor';
let uri = process.env.MONGO_URL2; //"mongodb://localhost:27017/avusd_data_collection";
//uri = "mongodb://localhost:27017/avusd_data_collection";
//console.log(uri);
let db2 = new MongoInternals.RemoteCollectionDriver(uri);
let collection = new Mongo.Collection("records", {_driver: db2});
Meteor.Records = collection;
// let results = collection.find({deviceId: "1e3e99ef-adf4-4aa2-8784-205bc60f0ce3"}).fetch();
// console.log(results);

90
server/logging.js Normal file
View File

@@ -0,0 +1,90 @@
import FileRotateTransport from "winston-daily-rotate-file";
import winston from "winston";
import moment from "moment";
import _ from 'underscore';
let production = (process.env.NODE_ENV === "production");
let logPath = process.env.LOG_PATH;
let fileTransport = logPath ? new FileRotateTransport({
format: winston.format.combine(
winston.format.simple(),
winston.format.printf((info) => {
return moment(info.timestamp).format('YYYY-MM-DD hh:mm:ss SSSS') + " " + info.message;
})
),
auditFile: "audit.json",
maxSize: '1m',
maxFiles: 20,
dirname: logPath,
filename: 'DistrictCentral-%DATE%.log',
dateFormat: "YYYY-MM-DD"
}) : null;
let consoleColorTransport = new winston.transports.Console({
format: winston.format.combine(
winston.format.cli()
),
});
let consoleTransport = new winston.transports.Console({
format: winston.format.combine(
winston.format.simple()
),
});
let transports = production ? (logPath ? [fileTransport] : [consoleTransport]) : (logPath ? [fileTransport, consoleColorTransport] : [consoleColorTransport]);
//let transports = [fileTransport, consoleTransport];
//TODO: Use GrayLog or SysLog and interface with a log server.
/* Use GrayLog2 Transport
const Graylog2 = require('winston-graylog2');
logger.add(new Graylog2(options));
*/
/* Use Syslog Transport
npm install winston-syslog
const winston = require('winston');
//
// Requiring `winston-syslog` will expose
// `winston.transports.Syslog`
//
require('winston-syslog').Syslog;
winston.add(new winston.transports.Syslog(options));
*/
// Setup the logger.
let logger = winston.createLogger({
level: production ? 'info' : 'silly',
// format: winston.format.combine(
// winston.format.timestamp({format: 'YYYY-MM-DD hh:mm:ss SSSS'})
// ),
format: winston.format(function(info) {
//Add a timestamp to the info structure.
info.timestamp = new Date();
return info;
})(),
defaultMeta: {app: 'DistrictCentral'},
transports: transports
});
let consoleLogOriginal = console.log;
// Override the log and error functions.
console.log = function(d) {
// For some reason Meteor requires the original console.log for the initial listening log output. If that isn't performed then Meteor never starts properly.
if(arguments.length === 1 && arguments[0] === 'LISTENING') {
return consoleLogOriginal.call(console, 'LISTENING');
}
else logger.log("debug", _.isObject(d) ? JSON.stringify(d) : d);
}
console.info = function(d) {
logger.log("info", _.isObject(d) ? JSON.stringify(d) : d);
}
console.warn = function(d) {
logger.log("warn", _.isObject(d) ? JSON.stringify(d) : d);
}
console.error = function(e) {
logger.log("error", e.stack || e);
}

View File

@@ -1,40 +1,35 @@
import './DataCollection.js';
import '../imports/api/';
import './google-oauth.js';
import '/imports/startup/accounts-config.js';
import './logging';
import url from 'url';
//import './google-oauth.js';
import connectRoute from 'connect-route';
/* Did not work at all.. not sure why.
let WebSocketServer = require("ws").Server;
//var wss = new WebSocketServer({ port: env.PORT });
let wss = new WebSocketServer({host: '192.168.3.101', port: 3001});
console.log("Starting WS");
wss.on("connection", function (ws) {
console.log("WS Open");
ws.on("message", function(data) {
console.log(data);
ws.send("Pong");
}).on("error", (err) => {
console.log(err);
})
}).on('error', (err) => {
console.error(err);
});
// const net = require('net');
//Some basic testing of detecting which mode the app is running under...
//
// const server = net.createServer((socket) => {
// socket.on('data', (data) => {
// console.log(data.toString());
// socket.write("Pong");
// });
// }).on('error', (err) => {
// console.error(err);
// });
// if(process.env.NODE_ENV === "development") {
// console.log("In Dev Mode");
// }
// else if(process.env.NODE_ENV === "production") {
// console.log("In Prod Mode");
// }
// else {
// console.log("No idea what mode we are in!");
// }
//TEST LOG OUTPUT...
// let obj = {test: 'abc'};
// console.log("Test Output");
// console.log(obj);
//
// try {
// throw Exception("Error!!");
// }
// catch(err) {
// console.error(err);
// }
*/