New login screen

- Basic user page
This commit is contained in:
2020-10-24 01:09:17 +02:00
parent edaff8a3ec
commit 8a4b0b1e13
24 changed files with 358 additions and 34 deletions

View File

@@ -1979,6 +1979,14 @@
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY="
},
"@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha1-YE69GJvDvDShVIaJQE5hoqSqyJY=",
"requires": {
"moment": "*"
}
},
"@types/node": {
"version": "12.12.67",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.67.tgz",

View File

@@ -22,10 +22,12 @@
"@angular/router": "~10.1.5",
"@types/chart.js": "^2.9.27",
"@types/mapbox-gl": "^1.12.5",
"@types/moment": "^2.13.0",
"chart.js": "^2.9.4",
"eva-icons": "^1.1.3",
"geojson": "^0.5.0",
"mapbox-gl": "^1.12.0",
"moment": "^2.29.1",
"ng2-charts": "^2.4.2",
"ngx-mapbox-gl": "^4.8.1",
"rxjs": "~6.6.0",

View File

@@ -15,6 +15,33 @@ export interface IBeat {
createdAt?: Date;
}
export enum UserType {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest'
}
export interface IUser {
name: string;
password: string;
salt: string;
type: UserType;
lastLogin: Date;
twoFASecret?: string;
createdAt?: Date;
}
export interface IPhone {
androidId: string;
displayName: string;
modelName: string;
operatingSystem: string;
architecture: string;
user: IUser;
updatedAt?: Date;
createdAt?: Date;
}
export interface ITimespan {
from?: number;
to?: number;
@@ -29,9 +56,20 @@ export class APIService {
username: string;
time: ITimespan | undefined;
showFilter = true;
// Cached data
beats: IBeat[];
phones: IPhone[];
user: IUser = {
name: '',
lastLogin: new Date(2020, 3, 1),
password: '',
salt: '',
type: UserType.GUEST,
createdAt: new Date(),
twoFASecret: ''
};
// Events when new data got fetched
beatsEvent: BehaviorSubject<IBeat[]> = new BehaviorSubject([]);
@@ -43,15 +81,15 @@ export class APIService {
constructor(private httpClient: HttpClient) { }
async login(username: string, password: string): Promise<ILogin> {
return new Promise<ILogin>((resolve, reject) => {
console.log('POST');
return new Promise<ILogin>(async (resolve, reject) => {
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
.subscribe(token => {
.subscribe(async token => {
console.log(token);
this.token = (token as ILogin).token;
this.username = username;
await this.getPhones();
await this.getUserInfo();
this.loginEvent.next(true);
resolve(token as ILogin);
});
@@ -73,8 +111,6 @@ export class APIService {
params = params.set('to', this.time.to.toString());
}
console.log(params);
this.httpClient.get(this.API_ENDPOINT + '/beat', { responseType: 'json', headers, params })
.subscribe(beats => {
this.beats = beats as IBeat[];
@@ -85,6 +121,56 @@ export class APIService {
});
}
async getUserInfo(): Promise<IUser> {
return new Promise<IUser>((resolve, reject) => {
if (this.token === undefined) { reject([]); }
const headers = new HttpHeaders({ token: this.token });
this.fetchingDataEvent.next(true);
this.httpClient.get(this.API_ENDPOINT + '/user', { responseType: 'json', headers })
.subscribe(user => {
this.user = user as IUser;
this.fetchingDataEvent.next(false);
resolve(user as IUser);
});
});
}
async getPhones(): Promise<IPhone[]> {
return new Promise<IPhone[]>((resolve, reject) => {
if (this.token === undefined) { reject([]); }
const headers = new HttpHeaders({ token: this.token });
this.fetchingDataEvent.next(true);
this.httpClient.get(this.API_ENDPOINT + '/phone', { responseType: 'json', headers })
.subscribe(phones => {
this.phones = phones as IPhone[];
this.fetchingDataEvent.next(false);
resolve(phones as IPhone[]);
});
});
}
async getPhone(phoneId: string): Promise<{ IPhone, IBeat }> {
return new Promise<{ IPhone, IBeat }>((resolve, reject) => {
if (this.token === undefined) { reject([]); }
const headers = new HttpHeaders({ token: this.token });
this.fetchingDataEvent.next(true);
this.httpClient.get(this.API_ENDPOINT + '/phone/' + phoneId, { responseType: 'json', headers })
.subscribe(phones => {
this.fetchingDataEvent.next(false);
resolve(phones as {IPhone, IBeat});
});
});
}
hasSession(): boolean {
return this.token !== undefined;
}

View File

@@ -4,6 +4,7 @@ import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
import { UserComponent } from './user/user.component';
const routes: Routes = [
{
@@ -21,6 +22,10 @@ const routes: Routes = [
{
path: 'map',
component: MapComponent
},
{
path: 'user',
component: UserComponent
}
];

View File

@@ -1,13 +1,14 @@
<div id="header">
<div id="header" *ngIf="this.api.loginEvent.value">
<ul class="navbar">
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active" >Dashboard</a></li>
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active" >Map</a></li>
<li><a [routerLink]="['/settings']" routerLinkActive="router-link-active" >Settings</a></li>
<li class="navbar-right"><a href="#about">{{this.api.username}}</a></li>
<li class="navbar-right"><a [routerLink]="['/user']" routerLinkActive="router-link-active" >{{this.api.username}}</a></li>
<li class="navbar-right"><a [routerLink]="['/settings']" routerLinkActive="router-link-active" *ngIf="this.api.user.type == 'admin'">Admin settings</a></li>
</ul>
</div>
<div class="header-spacer"></div>
<app-filter></app-filter>
<router-outlet></router-outlet>
<div id="loadingOverlay" [ngClass]="{show: this.showOverlay, gone: !this.showOverlay}">
<div>

View File

@@ -1,3 +1,4 @@
@import "../styles.scss";
#header {
position: fixed;
@@ -9,7 +10,7 @@
padding-bottom: 0.8rem;
background-color: #1d1d1dd9;
backdrop-filter: blur(30px);
box-shadow: 10px 10px 50px 0px rgba(0,0,0,0.85);
box-shadow: 10px 10px 50px 0px rgba(0, 0, 0, 0.85);
& ul {
display: inline;
@@ -39,7 +40,7 @@
left: 0;
z-index: 9999;
backdrop-filter: blur(40px);
background-color: rgba(0,0,0,0.75);
background-color: rgba(0, 0, 0, 0.75);
width: 100vw;
height: 30vh;
vertical-align: middle;
@@ -57,17 +58,17 @@
.show {
display: flex !important;
animation: showOverlay .2s ease-in-out;
animation: showOverlay 0.2s ease-in-out;
}
@keyframes showOverlay {
from {
backdrop-filter: blur(0px);
background-color: rgba(0,0,0,0);
background-color: rgba(0, 0, 0, 0);
}
to {
backdrop-filter: blur(10px);
background-color: rgba(0,0,0,0.8);
background-color: rgba(0, 0, 0, 0.8);
}
}

View File

@@ -19,7 +19,7 @@ export class AppComponent implements OnInit{
}
async ngOnInit(): Promise<void> {
await this.api.login('admin', '$1KDaNCDlyXAOg');
//await this.api.login('Nicolas', '$1KDaNCDlyXAOg');
return;
}
}

View File

@@ -12,6 +12,7 @@ import { DashboardComponent } from './dashboard/dashboard.component';
import { FilterComponent } from './filter/filter.component';
import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
import { UserComponent } from './user/user.component';
@NgModule({
declarations: [
@@ -19,7 +20,8 @@ import { MapComponent } from './map/map.component';
LoginComponent,
DashboardComponent,
MapComponent,
FilterComponent
FilterComponent,
UserComponent
],
imports: [
BrowserModule,

View File

@@ -28,7 +28,7 @@ export class DashboardComponent implements AfterViewInit {
time: {
parser: 'MM/DD/YYYY HH:mm:ss',
round: 'minute',
tooltipFormat: 'll HH:mm'
tooltipFormat: 'LL HH:mm'
},
scaleLabel: {
display: true,

View File

@@ -1,4 +1,4 @@
<div id="filter">
<div id="filter" [ngClass]="{hide: !this.api.showFilter}" *ngIf="this.api.loginEvent.value">
<h3>Filter</h3>
<select (change)="update($event.target.value)">
<option value="-1">Today</option>

View File

@@ -16,6 +16,12 @@
border-top-right-radius: 10px;
box-shadow: 10px 10px 50px 0px rgba(0, 0, 0, 0.9);
left: calc(50% - 30vw / 2);
transition: .5s ease-in-out;
transform: translateX(0);
}
.hide {
transform: translateY(5rem) !important;
}
h3 {

View File

@@ -9,7 +9,7 @@ import * as moment from 'moment';
})
export class FilterComponent implements OnInit {
constructor(private api: APIService) { }
constructor(public api: APIService) { }
ngOnInit(): void {
}

View File

@@ -4,13 +4,17 @@
<form>
<div id="username">
<label>Username</label><br>
<input type="text" name="username" placeholder="Username" [(ngModel)]="username">
</div>
<div id="password">
<label>Password</label><br>
<input type="password" name="password" placeholder="Password" [(ngModel)]="password">
</div>
<button (click)="perfomLogin()">Login</button>
</form>
</div>
<div id="lines">
<div class="line"></div>
<div class="line"></div>
<div class="line"></div>
</div>

View File

@@ -1,13 +1,112 @@
@import '../../styles.scss';
#login {
position: relative;
margin: 0 auto;
display: block;
height: fit-content;
width: fit-content;
padding: 5rem;
padding: 3rem;
border-radius: 20px;
background: #1d1d1d;
background: $header-background;
backdrop-filter: blur(10px);
text-align: center;
box-shadow: 20px 20px 60px #191919,
-20px -20px 60px #212121;
}
& form {
margin-top: 2rem;
& div {
margin-top: 1rem;
& input {
background-color: $secondary-color;
color: $foreground-color;
border: none;
border-bottom: 2px solid $foreground-color;
border-radius: 5px;
padding: .6rem;
padding-top: .8rem;
padding-bottom: .8rem;
transition: .3s ease-in-out;
}
& input:focus {
border: none !important;
padding: 1rem;
}
}
& button {
padding: 1rem;
background-color: $secondary-color;
color: $foreground-color;
margin-top: 2rem;
border: none;
transition: .5s ease-in-out;
font-size: 13pt;
}
& button:hover {
padding-left: 2rem;
padding-right: 2rem;
box-shadow: 20px 20px 60px #19191919,
-20px -20px 60px #21212119;
}
}
}
@keyframes run {
0% {
top: -50%;
}
100% {
top: 110%;
}
}
#lines {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
margin: auto;
z-index: -5;
width: 90vw;
.line {
position: absolute;
width: 1px;
height: 100%;
top: 0;
left: 50%;
background: rgba(255, 255, 255, 0.1);
overflow: hidden;
&::after {
content: '';
display: block;
position: absolute;
height: 15vh;
width: 100%;
top: -50%;
left: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #ffffff 75%, #ffffff 100%);
animation: run 3s 0s infinite;
animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(0.4, 0.26, 0, 0.97);
}
&:nth-child(1) {
margin-left: -25%;
&::after {
animation-delay: 2s;
}
}
&:nth-child(3) {
margin-left: 25%;
&::after {
animation-delay: 2.5s;
}
}
}
}

View File

@@ -1,13 +1,12 @@
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'">
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'" [zoom]="[10]" [center]="[12.7, 50.8]">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-layer
id="locHisotryLines"
type="line"
source="locHistory"
[zoom]="3"
[paint]="{
'line-color': '#ff0000',
'line-width': 4
'line-width': 3
}"
>
</mgl-layer>

View File

@@ -0,0 +1,14 @@
<div id="user">
<h1>{{this.api.user.name}}
<span class="adminState" *ngIf="this.api.user.type == 'admin'">Admin &#2B50;</span>
</h1>
<p class="lastLogin">Last login was {{lastLogin}}</p><br>
<h2>Devices</h2>
<ul class="phoneListing">
<li *ngFor="let phone of this.api.phones">
<h2>{{phone.displayName}}</h2>
<p>{{phone.modelName}}</p>
</li>
</ul>
</div>

View File

@@ -0,0 +1,21 @@
@import '../../styles.scss';
#user {
margin-top: 3rem;
margin-left: 20rem;
margin-right: 20rem;
}
.adminState {
padding-left: 1rem;
font-weight: bolder;
color: gold;
font-size: 12pt;
}
.phoneListing {
list-style: none;
padding: 1.5rem;
border-radius: 10px;
background-color: $darker;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserComponent } from './user.component';
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UserComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,33 @@
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
import { APIService } from '../api.service';
import * as moment from 'moment';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements AfterContentInit, OnDestroy {
lastLogin: string;
constructor(public api: APIService) {
this.api.loginEvent.subscribe(status => {
if (status) {
this.lastLogin = moment(this.api.user.lastLogin).fromNow();
console.log(this.lastLogin);
}
});
}
ngAfterContentInit(): void {
this.api.showFilter = false;
console.log(this.api.showFilter);
}
ngOnDestroy(): void {
this.api.showFilter = true;
}
}

View File

@@ -4,7 +4,8 @@
/* Color palette */
$primary-color: #1d1d1d;
$secondary-color: #1c1c1c;
$secondary-color: #2d2d2d;
$darker: #151515;
$foreground-color: #fff;
/* Misc */