First commit
24
app/frontend/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# tinycheck-new
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
app/frontend/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
12248
app/frontend/package-lock.json
generated
Normal file
48
app/frontend/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "tinycheck-new",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --copy --port=4202",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fnando/sparkline": "^0.3.10",
|
||||
"axios": "^0.20.0",
|
||||
"core-js": "^3.6.5",
|
||||
"sass": "^1.27.0",
|
||||
"sass-loader": "^10.0.4",
|
||||
"simple-keyboard": "^2.30.25",
|
||||
"vue": "^2.6.12",
|
||||
"vue-router": "^3.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.6",
|
||||
"@vue/cli-plugin-eslint": "~4.5.6",
|
||||
"@vue/cli-service": "~4.5.6",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.9.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
3
app/frontend/public/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.pythonPath": "/usr/local/opt/python@3.8/bin/python3.8"
|
||||
}
|
30
app/frontend/src/App.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view />
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import './assets/spectre.min.css';
|
||||
@import './assets/custom.css';
|
||||
|
||||
/* Face style for router stuff. */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition-duration: 0.3s;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.title = 'TinyCheck Frontend'
|
||||
</script>
|
||||
|
636
app/frontend/src/assets/custom.css
Normal file
@ -0,0 +1,636 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lobster';
|
||||
font-weight: normal;
|
||||
src: url('fonts/lobster.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto-Bold";
|
||||
src: url("fonts/Roboto-Bold.eot"); /* IE9 Compat Modes */
|
||||
src: url("fonts/Roboto-Bold.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
|
||||
url("fonts/Roboto-Bold.otf") format("opentype"), /* Open Type Font */
|
||||
url("fonts/Roboto-Bold.svg") format("svg"), /* Legacy iOS */
|
||||
url("fonts/Roboto-Bold.ttf") format("truetype"), /* Safari, Android, iOS */
|
||||
url("fonts/Roboto-Bold.woff") format("woff"), /* Modern Browsers */
|
||||
url("fonts/Roboto-Bold.woff2") format("woff2"); /* Modern Browsers */
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto-Regular";
|
||||
src: url("fonts/Roboto-Regular.eot"); /* IE9 Compat Modes */
|
||||
src: url("fonts/Roboto-Regular.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
|
||||
url("fonts/Roboto-Regular.otf") format("opentype"), /* Open Type Font */
|
||||
url("fonts/Roboto-Regular.svg") format("svg"), /* Legacy iOS */
|
||||
url("fonts/Roboto-Regular.ttf") format("truetype"), /* Safari, Android, iOS */
|
||||
url("fonts/Roboto-Regular.woff") format("woff"), /* Modern Browsers */
|
||||
url("fonts/Roboto-Regular.woff2") format("woff2"); /* Modern Browsers */
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Roboto-Regular";
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#qrcode {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-top:10px;
|
||||
margin-left:5px;
|
||||
border:1px #000;
|
||||
border-radius:5px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0;
|
||||
box-shadow: 0 0.25rem 1rem rgba(48,55,66,.15);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
font-family: "Roboto";
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-family: 'Lobster';
|
||||
font-size:30px;
|
||||
}
|
||||
|
||||
/*
|
||||
The min Chrome with is not 480 but 500 so we have to define our app div
|
||||
with 480 width on a 480 screen.
|
||||
*/
|
||||
@media only screen and (max-width: 500px) {
|
||||
|
||||
#app {
|
||||
height: 320px;
|
||||
display:block;
|
||||
width: 480px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#tinycheck-logo {
|
||||
width:370px;
|
||||
}
|
||||
|
||||
.apcard {
|
||||
width: 400px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 170px;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sparklines-container {
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
flex-wrap: wrap;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
align-content:center;
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.keyboardinput {
|
||||
padding:15px;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-family: "lobster";
|
||||
font-weight: lighter;
|
||||
font-size: 28px;
|
||||
margin-bottom: .5em;
|
||||
margin-top: .5em;
|
||||
color:#FFF;
|
||||
text-shadow: 1px 2px 3px #0000005c;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.report-wrapper {
|
||||
width:90%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.device-ctx {
|
||||
width:90%;
|
||||
margin:20px 0px 20px 10px;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
width:90%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.high-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e53935;
|
||||
}
|
||||
|
||||
.med-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f1602b;
|
||||
}
|
||||
|
||||
.low-wrapper, .none-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #56ab2f;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 501px) {
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
position:absolute;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.apcard {
|
||||
width: 500px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: fit-content;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#tinycheck-logo {
|
||||
width:500px;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-family: "lobster";
|
||||
font-weight: lighter;
|
||||
font-weight: bold;
|
||||
margin-bottom: .5em;
|
||||
margin-top: .5em;
|
||||
color:#FFF;
|
||||
text-shadow: 1px 2px 3px #0000005c;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.report-wrapper {
|
||||
width:60%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.device-ctx {
|
||||
padding:15px;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.high-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #e53935, #e35d5b);
|
||||
}
|
||||
|
||||
.med-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #ff4b1f, #ff9068);
|
||||
}
|
||||
|
||||
.low-wrapper, .none-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #56ab2f, #a8e063);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.width-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: fit-content;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.light-grey {
|
||||
color:#999;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-size:40px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f7f8f9;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-left:40px;
|
||||
padding-right:40px;
|
||||
padding-bottom:40px;
|
||||
}
|
||||
|
||||
.container-padding-right {
|
||||
padding-left:40px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
text-transform: uppercase;
|
||||
color : #999;
|
||||
font-size:12px;
|
||||
display: block;
|
||||
padding-bottom:10px;
|
||||
padding-top:30px;
|
||||
}
|
||||
|
||||
.keyboardinput {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 20px;
|
||||
font-size: 20px;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.legend {
|
||||
color:#9a9a9a;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #CCC;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Spectre CSS tweaks */
|
||||
|
||||
.btn {
|
||||
height: fit-content;
|
||||
border-radius: 5px;
|
||||
padding: .4rem .7rem;
|
||||
font-size: 1rem;
|
||||
color: #4c4c4c;
|
||||
background:#f7f8f9;
|
||||
border: 1px solid #4c4c4c;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
height: fit-content;
|
||||
border-radius: 5px;
|
||||
padding: .4rem .7rem;
|
||||
font-size: 1rem;
|
||||
color: #4c4c4c;
|
||||
background:#FFF;
|
||||
border: 1px solid #4c4c4c;
|
||||
}
|
||||
|
||||
.btn.active,.btn:active {
|
||||
height: fit-content;
|
||||
border-radius: 5px;
|
||||
padding: .4rem .7rem;
|
||||
font-size: 1rem;
|
||||
color: #4c4c4c;
|
||||
background:#FFF;
|
||||
border: 1px solid #4c4c4c;
|
||||
}
|
||||
|
||||
.btn.btn-light {
|
||||
background: #f9f9f9;
|
||||
border-color: #c1c1c1;
|
||||
color: #c1c1c1;
|
||||
}
|
||||
|
||||
.loadingsplash {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
animation: loading .5s infinite linear;
|
||||
background: 0 0;
|
||||
border: .1rem solid #000;
|
||||
border-radius: 50%;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
content: "";
|
||||
display: block;
|
||||
height: .8rem;
|
||||
left: 50%;
|
||||
margin-left: -.4rem;
|
||||
margin-top: -.4rem;
|
||||
opacity: 1;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: .8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loading.loading-lg::after {
|
||||
height: 1.6rem;
|
||||
margin-left: -.8rem;
|
||||
margin-top: -.8rem;
|
||||
width: 1.6rem;
|
||||
}
|
||||
|
||||
.divider-vert[data-content]::after, .divider[data-content]::after {
|
||||
background: #f7f8f9;
|
||||
}
|
||||
|
||||
.white-bg[data-content]::after, .white-bg[data-content]::after {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
#sparkline {
|
||||
stroke: #e8e8e8;
|
||||
fill: #f1f1f1;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.capture-wrapper {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn.btn-primary {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #333;
|
||||
color: #FAFAFA;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.btn.btn-primary:hover {
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgb(87, 87, 87);
|
||||
color: #FAFAFA;
|
||||
background-color: rgb(87, 87, 87);
|
||||
}
|
||||
|
||||
.btn.btn-primary:active {
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgb(87, 87, 87);
|
||||
color: #FAFAFA;
|
||||
background-color: rgb(87, 87, 87);
|
||||
}
|
||||
|
||||
.btn-report-high {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #e34b49;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-high:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #e34b49;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-moderate {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ff4b1f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-moderate:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ff4b1f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-low {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #56ab2f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-low:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #56ab2f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-low-light {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ffffff;
|
||||
background-color:transparent;
|
||||
margin-right:10px;
|
||||
}
|
||||
|
||||
.btn-report-low-light:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ffffff;
|
||||
background-color:transparent;
|
||||
margin-right:10px;
|
||||
}
|
||||
|
||||
.alert-body {
|
||||
background-color: #FFF;
|
||||
list-style: none;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.alert-body>.title {
|
||||
display: block;
|
||||
padding: 5px 5px 5px 10px;
|
||||
}
|
||||
|
||||
.high-label {
|
||||
background-color: #e53d38;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px 0px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.moderate-label {
|
||||
background-color: #ff7e33eb;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px 0px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.low-label {
|
||||
background-color: #4fce0eb8;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px 0px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.description>i {
|
||||
font-style: inherit;
|
||||
color: #353535;
|
||||
padding: 2px 5px 2px 5px;
|
||||
border-radius: 5px;
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.alert-id {
|
||||
background-color: #636363;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 0px 3px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
margin-top:20px;
|
||||
margin-bottom:20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#controls-analysis {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.lobster {
|
||||
font-family: 'lobster';
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.wifi-login {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.frame-download {
|
||||
width:1px;
|
||||
height:1px;
|
||||
border:0;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Used by the legend on analysis */
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn ease 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity:0;
|
||||
}
|
||||
100% {
|
||||
opacity:1;
|
||||
}
|
||||
}
|
BIN
app/frontend/src/assets/fonts/Roboto-Bold.eot
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Bold.otf
Normal file
11535
app/frontend/src/assets/fonts/Roboto-Bold.svg
Normal file
After Width: | Height: | Size: 805 KiB |
BIN
app/frontend/src/assets/fonts/Roboto-Bold.ttf
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Bold.woff
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Bold.woff2
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.eot
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.otf
Normal file
11080
app/frontend/src/assets/fonts/Roboto-Regular.svg
Normal file
After Width: | Height: | Size: 784 KiB |
BIN
app/frontend/src/assets/fonts/Roboto-Regular.ttf
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.woff
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.woff2
Normal file
BIN
app/frontend/src/assets/fonts/lobster.ttf
Normal file
BIN
app/frontend/src/assets/icon.png
Normal file
After Width: | Height: | Size: 28 KiB |
4
app/frontend/src/assets/icon_plug_usb.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="112" height="195" viewBox="0 0 112 195" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="3.5" y1="3.5" x2="3.50001" y2="191.5" stroke="black" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="7" width="105" height="195" fill="#F7F8F9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 294 B |
16
app/frontend/src/assets/icon_spinner.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="40px" height="40px" viewBox="0 0 40 40" enable-background="new 0 0 40 40" xml:space="preserve">
|
||||
<path opacity="0.2" fill="#000" d="M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946
|
||||
s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634
|
||||
c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z"/>
|
||||
<path fill="#f7f8f9" d="M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0
|
||||
C22.32,8.481,24.301,9.057,26.013,10.047z">
|
||||
<animateTransform attributeType="xml"
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 20 20"
|
||||
to="360 20 20"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 970 B |
5
app/frontend/src/assets/icon_success.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="106" height="106" viewBox="0 0 106 106" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="53" cy="53" r="53" fill="#40D8A1"/>
|
||||
<path d="M29 52.5L47.5 70.5" stroke="white" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="79" y1="40.0711" x2="48.0711" y2="71" stroke="white" stroke-width="10" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 377 B |
6
app/frontend/src/assets/icon_usb.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="548" height="199" viewBox="0 0 548 199" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="403" y="27" width="142" height="145" rx="8" fill="white" stroke="black" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M0 30C0 13.4315 13.4315 0 30 0H428C432.418 0 436 3.58172 436 8V191C436 195.418 432.418 199 428 199H30C13.4315 199 0 185.569 0 169V30Z" fill="black"/>
|
||||
<rect x="477" y="55" width="26" height="26" fill="white" stroke="black" stroke-width="6"/>
|
||||
<rect x="477" y="117" width="26" height="26" fill="white" stroke="black" stroke-width="6"/>
|
||||
</svg>
|
After Width: | Height: | Size: 602 B |
11
app/frontend/src/assets/loading.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<circle cx="50" cy="50" r="0" fill="none" stroke="#f3f3f3" stroke-width="2">
|
||||
<animate attributeName="r" repeatCount="indefinite" dur="1.4925373134328357s" values="0;30" keyTimes="0;1" keySplines="0 0.2 0.8 1" calcMode="spline" begin="-0.7462686567164178s"></animate>
|
||||
<animate attributeName="opacity" repeatCount="indefinite" dur="1.4925373134328357s" values="1;0" keyTimes="0;1" keySplines="0.2 0 0.8 1" calcMode="spline" begin="-0.7462686567164178s"></animate>
|
||||
</circle>
|
||||
<circle cx="50" cy="50" r="0" fill="none" stroke="#d8dddf" stroke-width="2">
|
||||
<animate attributeName="r" repeatCount="indefinite" dur="1.4925373134328357s" values="0;30" keyTimes="0;1" keySplines="0 0.2 0.8 1" calcMode="spline"></animate>
|
||||
<animate attributeName="opacity" repeatCount="indefinite" dur="1.4925373134328357s" values="1;0" keyTimes="0;1" keySplines="0.2 0 0.8 1" calcMode="spline"></animate>
|
||||
</circle>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/frontend/src/assets/logo.png
Normal file
After Width: | Height: | Size: 88 KiB |
1
app/frontend/src/assets/spectre.min.css
vendored
Normal file
10
app/frontend/src/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
Vue.config.productionTip = true
|
||||
Vue.config.devtools = true
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
62
app/frontend/src/router/index.js
Normal file
@ -0,0 +1,62 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'loader',
|
||||
component: () => import('../views/splash-screen.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: () => import('../views/home.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/wifi-select',
|
||||
name: 'wifi-select',
|
||||
component: () => import('../views/wifi-select.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/generate-ap',
|
||||
name: 'generate-ap',
|
||||
component: () => import('../views/generate-ap.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/capture',
|
||||
name: 'capture',
|
||||
component: () => import('../views/capture.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/save-capture',
|
||||
name: 'save-capture',
|
||||
component: () => import('../views/save-capture.vue'),
|
||||
props: true
|
||||
},
|
||||
{ path: '/analysis',
|
||||
name: 'analysis',
|
||||
component: () => import('../views/analysis.vue'),
|
||||
props: true
|
||||
},
|
||||
{ path: '/report',
|
||||
name: 'report',
|
||||
component: () => import('../views/report.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
60
app/frontend/src/views/SimpleKeyboard.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div :class="keyboardClass"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Keyboard from "simple-keyboard";
|
||||
import "simple-keyboard/build/css/index.css";
|
||||
|
||||
export default {
|
||||
name: "SimpleKeyboard",
|
||||
props: {
|
||||
keyboardClass: {
|
||||
default: "simple-keyboard",
|
||||
type: String
|
||||
},
|
||||
input: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
keyboard: null
|
||||
}),
|
||||
mounted() {
|
||||
this.keyboard = new Keyboard({
|
||||
onChange: this.onChange,
|
||||
onKeyPress: this.onKeyPress
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onChange(input) {
|
||||
this.$emit("onChange", input);
|
||||
},
|
||||
onKeyPress(button) {
|
||||
this.$emit("onKeyPress", button);
|
||||
|
||||
/**
|
||||
* If you want to handle the shift and caps lock buttons
|
||||
*/
|
||||
if (button === "{shift}" || button === "{lock}") this.handleShift();
|
||||
},
|
||||
handleShift() {
|
||||
let currentLayout = this.keyboard.options.layoutName;
|
||||
let shiftToggle = currentLayout === "default" ? "shift" : "default";
|
||||
|
||||
this.keyboard.setOptions({
|
||||
layoutName: shiftToggle
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
input(input) {
|
||||
this.keyboard.setInput(input);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
70
app/frontend/src/views/analysis.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<div v-if="question">
|
||||
<p>Do you want to analyze the captured communications?</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn" v-on:click="save_capture()">No, just save them</button> <button class="btn btn-primary" v-on:click="start_analysis()">Yes, let's do it</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="running">
|
||||
<img src="@/assets/loading.svg"/>
|
||||
<p class="legend" v-if="!long_waiting">Please wait during the analysis...</p>
|
||||
<p class="legend fade-in" v-if="long_waiting">Yes, it can take some time...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'analysis',
|
||||
data() {
|
||||
return {
|
||||
question: true,
|
||||
running: false,
|
||||
check_alerts: false,
|
||||
long_waiting: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
capture_token: String
|
||||
},
|
||||
methods: {
|
||||
start_analysis: function() {
|
||||
this.question = false
|
||||
this.running = true
|
||||
setTimeout(function () { this.long_waiting = true }.bind(this), 15000);
|
||||
axios.get(`/api/analysis/start/${this.capture_token}`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.message == "Analysis started")
|
||||
this.check_alerts = setInterval(() => { this.get_alerts(); }, 500);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
get_alerts: function() {
|
||||
axios.get(`/api/analysis/report/${this.capture_token}`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.message != "No report yet"){
|
||||
clearInterval(this.check_alerts);
|
||||
this.long_waiting = false;
|
||||
this.running = false;
|
||||
router.replace({ name: 'report', params: { alerts : response.data.alerts,
|
||||
device : response.data.device,
|
||||
capture_token : this.capture_token } });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
save_capture: function() {
|
||||
var capture_token = this.capture_token
|
||||
router.replace({ name: 'save-capture', params: { capture_token: capture_token } });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
100
app/frontend/src/views/capture.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="capture-wrapper">
|
||||
<svg id="sparkline" stroke-width="3" :width="sparkwidth" :height="sparkheight" v-if="sparklines"></svg>
|
||||
<div class="center">
|
||||
<div class="footer">
|
||||
<h3 class="timer">{{timer_hours}}:{{timer_minutes}}:{{timer_seconds}}</h3>
|
||||
<p>Intercepting the communications of {{device_name}}.</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn" :class="[ loading ? 'loading' : 'btn-primary', ]" v-on:click="stop_capture()">Stop the capture</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
import sparkline from '@fnando/sparkline'
|
||||
|
||||
export default {
|
||||
name: 'capture',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
timer_hours: "00",
|
||||
timer_minutes: "00",
|
||||
timer_seconds: "00",
|
||||
loading: false,
|
||||
stats_interval: false,
|
||||
chrono_interval: false,
|
||||
sparklines: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
capture_token: String,
|
||||
device_name: String
|
||||
},
|
||||
methods: {
|
||||
set_chrono: function() {
|
||||
this.chrono_interval = setInterval(() => { this.chrono(); }, 10);
|
||||
},
|
||||
stop_capture: function() {
|
||||
this.loading = true
|
||||
axios.get(`/api/network/ap/stop`, { timeout: 30000 })
|
||||
axios.get(`/api/capture/stop`, { timeout: 30000 })
|
||||
.then(response => (this.handle_finish(response.data)))
|
||||
},
|
||||
get_stats: function() {
|
||||
axios.get(`/api/capture/stats`, { timeout: 30000 })
|
||||
.then(response => (this.handle_stats(response.data)))
|
||||
},
|
||||
handle_stats: function(data) {
|
||||
if (data.packets.length) sparkline(document.querySelector("#sparkline"), data.packets);
|
||||
},
|
||||
handle_finish: function(data) {
|
||||
clearInterval(this.chrono_interval);
|
||||
clearInterval(this.stats_interval);
|
||||
if (data.status) {
|
||||
this.loading = false
|
||||
var capture_token = this.capture_token
|
||||
router.replace({ name: 'analysis', params: { capture_token: capture_token } });
|
||||
}
|
||||
},
|
||||
chrono: function() {
|
||||
var time = Date.now() - this.capture_start
|
||||
this.timer_hours = Math.floor(time / (60 * 60 * 1000));
|
||||
this.timer_hours = (this.timer_hours < 10) ? "0" + this.timer_hours : this.timer_hours
|
||||
time = time % (60 * 60 * 1000);
|
||||
this.timer_minutes = Math.floor(time / (60 * 1000));
|
||||
this.timer_minutes = (this.timer_minutes < 10) ? "0" + this.timer_minutes : this.timer_minutes
|
||||
time = time % (60 * 1000);
|
||||
this.timer_seconds = Math.floor(time / 1000);
|
||||
this.timer_seconds = (this.timer_seconds < 10) ? "0" + this.timer_seconds : this.timer_seconds
|
||||
},
|
||||
setup_sparklines: function() {
|
||||
axios.get(`/api/misc/config`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.sparklines){
|
||||
this.sparklines = true
|
||||
this.sparkwidth = window.screen.width + "px";
|
||||
this.sparkheight = Math.trunc(window.screen.height / 5) + "px";
|
||||
this.stats_interval = setInterval(() => { this.get_stats(); }, 500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
// Get the config for the sparklines.
|
||||
this.setup_sparklines()
|
||||
|
||||
// Start the chrono and get the first stats.
|
||||
this.capture_start = Date.now()
|
||||
this.set_chrono();
|
||||
}
|
||||
}
|
||||
</script>
|
119
app/frontend/src/views/generate-ap.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<div v-if="(error == false)">
|
||||
<div v-if="ssid_name">
|
||||
<div class="card apcard" v-on:click="generate_ap()">
|
||||
<div class="columns">
|
||||
<div class="column col-5">
|
||||
<center><img :src="ssid_qr" id="qrcode"></center>
|
||||
</div>
|
||||
<div class="divider-vert white-bg" data-content="OR"></div>
|
||||
<div class="column col-5"><br />
|
||||
<span class="light-grey">Network name: </span><br />
|
||||
<h4>{{ ssid_name }}</h4>
|
||||
<span class="light-grey">Network password: </span><br />
|
||||
<h4>{{ ssid_password }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br /><br /><br /><br /> <br /><br /><br /><br /><br /><br />
|
||||
<!-- Requite a CSS MEME for that shit :) -->
|
||||
<span class="legend">Tap the white frame to generate a new network.</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<img src="@/assets/loading.svg"/>
|
||||
<p class="legend">We generate an ephemeral network for you.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>
|
||||
<strong>Unfortunately, we got some issues.</strong>
|
||||
<br /><br />
|
||||
Please verify that you've two Wifi interfaces on your device<br />
|
||||
and restart it by clicking on the button below.<br />
|
||||
</p>
|
||||
<button class="btn" v-on:click="reboot()">Reboot the device</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'generate-ap',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
ssid_name: false,
|
||||
ssid_qr: false,
|
||||
ssid_password: false,
|
||||
capture_token: false,
|
||||
capture_start: false,
|
||||
interval: false,
|
||||
error: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generate_ap: function() {
|
||||
clearInterval(this.interval)
|
||||
this.ssid_name = false
|
||||
axios.get(`/api/network/ap/start`, { timeout: 30000 })
|
||||
.then(response => (this.show_ap(response.data)))
|
||||
},
|
||||
show_ap: function(data) {
|
||||
if (data.status) {
|
||||
this.ssid_name = data.ssid
|
||||
this.ssid_password = data.password
|
||||
this.ssid_qr = data.qrcode
|
||||
this.start_capture() // Start the capture before client connect.
|
||||
} else {
|
||||
this.error = true
|
||||
}
|
||||
},
|
||||
start_capture: function() {
|
||||
axios.get(`/api/capture/start`, { timeout: 30000 })
|
||||
.then(response => (this.get_capture_token(response.data)))
|
||||
},
|
||||
reboot: function() {
|
||||
axios.get(`/api/misc/reboot`, { timeout: 30000 })
|
||||
.then(response => { console.log(response)})
|
||||
},
|
||||
get_capture_token: function(data) {
|
||||
if (data.status) {
|
||||
this.capture_token = data.capture_token
|
||||
this.capture_start = Date.now()
|
||||
this.get_device()
|
||||
}
|
||||
},
|
||||
get_device: function() {
|
||||
this.interval = setInterval(() => {
|
||||
axios.get(`/api/device/get/${this.capture_token}`, { timeout: 30000 })
|
||||
.then(response => (this.check_device(response.data)))
|
||||
}, 500);
|
||||
},
|
||||
check_device: function(data) {
|
||||
if (data.status) {
|
||||
clearInterval(this.interval);
|
||||
var capture_token = this.capture_token
|
||||
var capture_start = this.capture_start
|
||||
var device_name = data.name
|
||||
router.replace({
|
||||
name: 'capture',
|
||||
params: {
|
||||
capture_token: capture_token,
|
||||
capture_start: capture_start,
|
||||
device_name: device_name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.generate_ap();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
24
app/frontend/src/views/home.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<h3 class="lobster">Welcome to TinyCheck.</h3>
|
||||
<p>We are going to help you to check your device.</p>
|
||||
<button class="btn btn-primary" v-on:click="next()">Let's start!</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
props: { saved_ssid: String, list_ssids: Array, internet: Boolean },
|
||||
methods: {
|
||||
next: function() {
|
||||
var saved_ssid = this.saved_ssid
|
||||
var list_ssids = this.list_ssids
|
||||
var internet = this.internet
|
||||
router.push({ name: 'wifi-select', params: { saved_ssid: saved_ssid, list_ssids: list_ssids, internet:internet } });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
115
app/frontend/src/views/report.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="results">
|
||||
<div v-if="alerts.high.length >= 1" class="high-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">You have {{ nb_translate(alerts.high.length) }} high alert,<br />your device seems to be compromised.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
|
||||
<button class="btn btn-report-high" @click="show_report=true;results=false;">Show the full report</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="alerts.moderate.length >= 1" class="med-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">You have {{ nb_translate(alerts.moderate.length) }} moderate alerts,<br />your device might be compromised.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
|
||||
<button class="btn btn-report-moderate" @click="show_report=true;results=false;">Show the full report</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="alerts.low.length >= 1" class="low-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">You have ony {{ nb_translate(alerts.moderate.low) }} low alerts,<br /> don't hesitate to check them.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
|
||||
<button class="btn btn-report-low" @click="show_report=true;results=false;">Show the full report</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="none-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">Everything looks fine, zero alerts.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="save_capture()">Save the capture</button>
|
||||
<button class="btn btn-report-low" v-on:click="new_capture()">Start a new capture</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="show_report" class="report-wrapper">
|
||||
<div class="device-ctx">
|
||||
<h3 style="margin: 0;">Report for {{device.name}}</h3>
|
||||
IP Address: {{device.ip_address}}<br />Mac Address: {{device.mac_address}}
|
||||
</div>
|
||||
<ul class="alerts">
|
||||
<li class="alert" v-for="alert in alerts.high" :key="alert.message">
|
||||
<span class="high-label">High</span><span class="alert-id">{{ alert.id }}</span>
|
||||
<div class="alert-body">
|
||||
<span class="title">{{ alert.title }}</span>
|
||||
<p class="description">{{ alert.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="alert" v-for="alert in alerts.moderate" :key="alert.message">
|
||||
<span class="moderate-label">Moderate</span><span class="alert-id">{{ alert.id }}</span>
|
||||
<div class="alert-body">
|
||||
<span class="title">{{ alert.title }}</span>
|
||||
<p class="description">{{ alert.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="alert" v-for="alert in alerts.low" :key="alert.message">
|
||||
<span class="low-label">Low</span><span class="alert-id">{{ alert.id }}</span>
|
||||
<div class="alert-body">
|
||||
<span class="title">{{ alert.title }}</span>
|
||||
<p class="description">{{ alert.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="columns" id="controls-analysis">
|
||||
<div class="column col-5">
|
||||
<button class="btn width-100" @click="$router.push('generate-ap')">Start a capture</button>
|
||||
</div>
|
||||
<div class="divider-vert column col-2" data-content="OR"></div>
|
||||
<div class="column col-5">
|
||||
<button class="btn btn btn-primary width-100" v-on:click="save_capture()">Save the report</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style>
|
||||
#app {
|
||||
overflow-y: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'report',
|
||||
data() {
|
||||
return {
|
||||
results: true,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
device: Object,
|
||||
alerts: Array,
|
||||
capture_token: String
|
||||
},
|
||||
methods: {
|
||||
save_capture: function() {
|
||||
var capture_token = this.capture_token
|
||||
router.replace({ name: 'save-capture', params: { capture_token: capture_token } });
|
||||
},
|
||||
new_capture: function() {
|
||||
router.push({ name: 'generate-ap' })
|
||||
},
|
||||
nb_translate: function(x) {
|
||||
var nbs = ['zero','one','two','three','four', 'five','six','seven','eight','nine', 'ten', 'eleven']
|
||||
try {
|
||||
return nbs[x];
|
||||
} catch (error)
|
||||
{
|
||||
return x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
200
app/frontend/src/views/save-capture.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="center" v-if="save_usb && init">
|
||||
<div class="canvas-anim" :class="{'anim-connect': !saved && !usb}" v-on:click="new_capture()">
|
||||
<div class="icon-spinner" v-if="!saved && usb"></div>
|
||||
<div class="icon-success" v-if="saved"></div>
|
||||
<div class="icon-usb"></div>
|
||||
<div class="icon-usb-plug"></div>
|
||||
</div>
|
||||
<p class="legend" v-if="!saved && !usb"><br />Please connect a USB key to save your capture.</p>
|
||||
<p class="legend" v-if="!saved && usb"><br />We are saving your capture.</p>
|
||||
<p class="legend" v-if="saved"><br />You can tap the USB key to start a new capture.</p>
|
||||
</div>
|
||||
<div class="center" v-else-if="!save_usb && init">
|
||||
<div>
|
||||
<p class="legend">The capture download is going to start...<br /><br /><br /></p>
|
||||
<button class="btn btn-primary" v-on:click="new_capture()">Start another capture</button>
|
||||
<iframe :src="download_url" class="frame-download"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.canvas-anim {
|
||||
height: 120px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
width: 205px;
|
||||
|
||||
&.anim-connect {
|
||||
width: 300px;
|
||||
|
||||
.icon-usb {
|
||||
-webkit-animation: slide-right 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) infinite alternate both;
|
||||
animation: slide-right 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) infinite alternate both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-usb {
|
||||
background: url('../assets/icon_usb.svg') no-repeat 0 0;
|
||||
background-size: 200px auto;
|
||||
display: block;
|
||||
height: 120px;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.icon-usb-plug {
|
||||
background: url('../assets/icon_plug_usb.svg') no-repeat 0 0;
|
||||
background-size: cover;
|
||||
display: block;
|
||||
height: 120px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
width: 55px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
background: url('../assets/icon_success.svg') no-repeat 0 0;
|
||||
background-size: 80px auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 120px;
|
||||
top: -25px;
|
||||
left: -40px;
|
||||
width: 80px;
|
||||
z-index: 10;
|
||||
-webkit-animation: scale-down-center 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
animation: scale-down-center 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
background: url('../assets/icon_spinner.svg') no-repeat 0 0;
|
||||
background-color: #f7f8f9;
|
||||
border-radius: 40px;
|
||||
display: block;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: -20px;
|
||||
width: 40px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-right {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(75px);
|
||||
transform: translateX(75px);
|
||||
}
|
||||
}
|
||||
@keyframes slide-right {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(75px);
|
||||
transform: translateX(75px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes scale-down-center {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale(0.5);
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
@keyframes scale-down-center {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale(0.5);
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'save-capture',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
usb: false,
|
||||
saved: false,
|
||||
save_usb: false,
|
||||
init: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
capture_token: String
|
||||
},
|
||||
methods: {
|
||||
check_usb: function() {
|
||||
axios.get(`/api/save/usb-check`, { timeout: 30000 })
|
||||
.then(response => {
|
||||
if(response.data.status) {
|
||||
this.usb = true
|
||||
clearInterval(this.interval)
|
||||
this.save_capture()
|
||||
}
|
||||
})
|
||||
},
|
||||
save_capture: function() {
|
||||
var capture_token = this.capture_token
|
||||
axios.get(`/api/save/save-capture/${capture_token}/usb`, { timeout: 30000 })
|
||||
.then(response => {
|
||||
if(response.data.status){
|
||||
this.saved = true
|
||||
this.timeout = setTimeout(() => router.push('/'), 60000);
|
||||
}
|
||||
})
|
||||
},
|
||||
new_capture: function() {
|
||||
clearTimeout(this.timeout);
|
||||
router.push({ name: 'generate-ap' })
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/misc/config`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.download_links){
|
||||
this.init = true
|
||||
this.save_usb = false
|
||||
this.download_url = `/api/save/save-capture/${this.capture_token}/url`
|
||||
} else {
|
||||
this.init = true
|
||||
this.save_usb = true
|
||||
this.interval = setInterval(() => { this.check_usb() }, 500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.load_config()
|
||||
}
|
||||
}
|
||||
</script>
|
53
app/frontend/src/views/splash-screen.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<img src="@/assets/logo.png" id="tinycheck-logo" />
|
||||
<div class="loading loading-lg loadingsplash"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'splash-screen',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
list_ssids: [],
|
||||
internet: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Check if the device is already connected to internet.
|
||||
internet_check: function() {
|
||||
axios.get(`/api/network/status`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if (response.data.internet) this.internet = true
|
||||
this.get_wifi_networks()
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
// Get the WiFi networks around the box.
|
||||
get_wifi_networks: function() {
|
||||
axios.get(`/api/network/wifi/list`, { timeout: 10000 })
|
||||
.then(response => (this.append_ssids(response.data.networks)))
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
// Handle the get_wifi_networks answer and call goto_home.
|
||||
append_ssids: function(networks) {
|
||||
this.list_ssids = networks
|
||||
this.goto_home()
|
||||
},
|
||||
// Pass the list of ssids and the internet status as a prop to the home view.
|
||||
goto_home: function() {
|
||||
var list_ssids = this.list_ssids
|
||||
var internet = this.internet
|
||||
router.replace({ name: 'home', params: { list_ssids: list_ssids, internet: internet } });
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.internet_check();
|
||||
}
|
||||
}
|
||||
</script>
|
166
app/frontend/src/views/wifi-select.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div :class="[ keyboard == false ? 'center' : '' ]">
|
||||
<div v-if="keyboard == false">
|
||||
<div v-if="have_internet">
|
||||
<p>You seem to be already connected to a network.<br />Do you want to use the current connection?</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn" @click="have_internet = false">No, use another</button> <button class="btn" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" @click="$router.push({ name: 'generate-ap' })">Yes, use it.</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="enter_creds" class="wifi-login">
|
||||
<div class="form-group">
|
||||
<select class="form-select" id="ssid-select" v-model="ssid">
|
||||
<option value="" selected>Wifi name</option>
|
||||
<option v-for="ssid in ssids" v-bind:key="ssid.ssid">
|
||||
{{ ssid.ssid }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-input" type="password" id="password" v-model="password" placeholder="Wifi password" v-on:click="keyboard = (virtual_keyboard)? true : false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn width-100" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" v-on:click="wifi_setup()">{{ btnval }}</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn width-100" :class="[ refreshing ? 'loading' : '' ]" v-on:click="refresh_wifi_list()">Refresh networks list</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p><strong>You seem to not be connected to Internet.</strong><br />Please configure the Wi-Fi connection.</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn btn-primary" @click="enter_creds = true">Ok, let's do that.</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<input :value="input" class="keyboardinput" @input="onInputChange" placeholder="Tap on the virtual keyboard to start">
|
||||
<SimpleKeyboard @onChange="onChange" @onKeyPress="onKeyPress" :input="input" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
import SimpleKeyboard from "./SimpleKeyboard";
|
||||
|
||||
export default {
|
||||
name: 'wifi-select',
|
||||
components: {
|
||||
SimpleKeyboard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
connecting: false,
|
||||
error: false,
|
||||
success: false,
|
||||
btnval: "Connect to it.",
|
||||
ssid: "",
|
||||
selected_ssid: false,
|
||||
password: "",
|
||||
keyboard: false,
|
||||
input: "",
|
||||
ssids: [],
|
||||
have_internet: false,
|
||||
enter_creds: false,
|
||||
virtual_keyboard: false,
|
||||
refreshing: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
saved_ssid: String,
|
||||
list_ssids: Array,
|
||||
internet: Boolean
|
||||
},
|
||||
methods: {
|
||||
wifi_connect: function() {
|
||||
axios.get(`/api/network/wifi/connect`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if (response.data.status) {
|
||||
this.success = true
|
||||
this.connecting = false
|
||||
this.btnval = "Wifi connected!"
|
||||
setTimeout(() => router.push('generate-ap'), 1000);
|
||||
} else {
|
||||
this.btnval = "Wifi not connected. Please retry."
|
||||
this.connecting = false
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
},
|
||||
wifi_setup: function() {
|
||||
if (this.ssid.length && this.password.length >= 8 ){
|
||||
axios.post(`/api/network/wifi/setup`, { ssid: this.ssid, password: this.password }, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.status) {
|
||||
this.connecting = true
|
||||
this.wifi_connect()
|
||||
} else {
|
||||
console.log(response.data.message)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/misc/config`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
this.virtual_keyboard = response.data.virtual_keyboard
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
},
|
||||
onChange(input) {
|
||||
this.input = input
|
||||
this.password = this.input;
|
||||
},
|
||||
onKeyPress(button) {
|
||||
if (button == "{enter}")
|
||||
this.keyboard = false
|
||||
},
|
||||
onInputChange(input) {
|
||||
this.input = input.target.value;
|
||||
},
|
||||
append_ssids: function(networks) {
|
||||
this.ssids = networks
|
||||
},
|
||||
refresh_wifi_list: function(){
|
||||
this.refreshing = true
|
||||
axios.get(`/api/network/wifi/list`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
this.refreshing = false
|
||||
this.append_ssids(response.data.networks)
|
||||
}).catch(error => {
|
||||
this.refreshing = false
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.load_config()
|
||||
|
||||
this.have_internet = (this.internet) ? true : false
|
||||
this.keyboard = false
|
||||
|
||||
if (typeof this.list_ssids == "object" && this.list_ssids.length != 0){
|
||||
this.ssids = this.list_ssids
|
||||
} else {
|
||||
this.refresh_wifi_list()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
11
app/frontend/vue.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: 'http://localhost:8040',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|