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

@@ -149,7 +149,8 @@ async function run() {
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/phone/:id', (req, res) => GetPhone(req, res));
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
app.get('/phone/:id', MW_User, (req, res) => GetPhone(req, res));
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));

View File

@@ -1,13 +1,16 @@
import { Response } from "express";
import { logger } from "../app";
import { LivebeatRequest } from "../lib/request";
import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model";
export async function GetPhone(req: LivebeatRequest, res: Response) {
const phoneId: String = req.params['id'];
// If none id provided, return all.
if (phoneId === undefined) {
res.status(400).send();
const phone = await Phone.find({ user: req.user?._id });
res.status(200).send(phone);
return;
}
@@ -18,7 +21,10 @@ export async function GetPhone(req: LivebeatRequest, res: Response) {
return;
}
res.status(200).send(phone);
// Get last beat
const lastBeat = await Beat.findOne({ phone: phone?._id }).sort({ createdAt: -1 });
res.status(200).send({ phone, lastBeat });
}
export async function PostPhone(req: LivebeatRequest, res: Response) {

View File

@@ -5,9 +5,15 @@ import { sign, decode, verify } from 'jsonwebtoken';
import { JWT_SECRET, logger } from "../app";
import { LivebeatRequest } from '../lib/request';
import { SchemaTypes } from "mongoose";
import { Phone } from "../models/phone/phone.model";
export async function GetUser(req: Request, res: Response) {
export async function GetUser(req: LivebeatRequest, res: Response) {
let user: any = req.user;
user.password = undefined;
user.salt = undefined;
user.__v = undefined;
res.status(200).send(user);
}
export async function DeleteUser(req: Request, res: Response) {
@@ -51,6 +57,9 @@ export async function LoginUser(req: Request, res: Response) {
// We're good. Create JWT token.
const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' });
user.lastLogin = new Date(Date.now());
await user.save();
logger.info(`User ${user.name} logged in.`)
res.status(200).send({ token });
}

View File

@@ -11,7 +11,8 @@ const schemaBeat = new Schema({
}, {
timestamps: {
createdAt: true
}
},
versionKey: false
});
export { schemaBeat };

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;
@@ -57,7 +58,7 @@
.show {
display: flex !important;
animation: showOverlay .2s ease-in-out;
animation: showOverlay 0.2s ease-in-out;
}
@keyframes showOverlay {

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 */