FrontEnd Angular - v0.0.1-alpha
This commit is contained in:
16
src/app/shared/auth/auth.service.spec.ts
Normal file
16
src/app/shared/auth/auth.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
118
src/app/shared/auth/auth.service.ts
Normal file
118
src/app/shared/auth/auth.service.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { first, firstValueFrom, Observable, Subject, take } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { HttpError } from '../model/httpError/httpError.model';
|
||||
import { Token } from '../model/token/token.model';
|
||||
import { User } from '../model/user/user.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
|
||||
private userAuthenticated!: User;
|
||||
|
||||
authSubject = new Subject<User|HttpError|null>();
|
||||
|
||||
readonly BACKEND_PATH = environment.backendPath;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
login(userAuthAtempt: User): void {
|
||||
this.validateUser(this.loginUser(userAuthAtempt));
|
||||
}
|
||||
|
||||
signup(userAuthAtempt: User): void {
|
||||
this.validateUser(this.createUser(userAuthAtempt));
|
||||
|
||||
}
|
||||
|
||||
autoLogin(): void {
|
||||
this.validateUser(this.validateSession());
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authSubject.next(null);
|
||||
this.destroySessions().subscribe()
|
||||
}
|
||||
|
||||
async getUserAccessToken(): Promise<Token | undefined> {
|
||||
if (this.userAuthenticated) {
|
||||
if ((!this.userAuthenticated.accessToken && this.refreshAccessToken ) ||
|
||||
(this.userAuthenticated.accessToken && this.userAuthenticated.accessToken.expirationDate < Date.now())) {
|
||||
this.userAuthenticated = <User>(await this.refreshAccessToken());
|
||||
}
|
||||
return this.userAuthenticated.accessToken;
|
||||
} else return
|
||||
}
|
||||
|
||||
private loginUser(userAuthAtempt: User): Observable<User|any> {
|
||||
|
||||
let loginParams = new URLSearchParams();
|
||||
loginParams.set("username", userAuthAtempt.username!);
|
||||
loginParams.set("password", userAuthAtempt.password!);
|
||||
|
||||
let headers = new HttpHeaders({
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
});
|
||||
|
||||
return this.http.post<User>(
|
||||
this.BACKEND_PATH + "/user/login",
|
||||
loginParams,
|
||||
{ headers: headers, withCredentials: true }
|
||||
).pipe(
|
||||
first()
|
||||
)
|
||||
}
|
||||
|
||||
private createUser(newUser: User) {
|
||||
return this.http.post<User>(
|
||||
this.BACKEND_PATH + "/user/signup",
|
||||
newUser,
|
||||
{ withCredentials: true }
|
||||
).pipe(
|
||||
first()
|
||||
)
|
||||
}
|
||||
|
||||
private validateUser(userAuthAtempt: Observable<User>) {
|
||||
userAuthAtempt.subscribe({
|
||||
next: userAuthentication => {
|
||||
this.userAuthenticated = <User>userAuthentication;
|
||||
this.authSubject.next(this.userAuthenticated);
|
||||
},
|
||||
error: err => {
|
||||
this.authSubject.next(<HttpError>err.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private refreshAccessToken() {
|
||||
return firstValueFrom(this.http.post(
|
||||
this.BACKEND_PATH + "/user/login/refresh",
|
||||
this.userAuthenticated.refreshToken,
|
||||
{ withCredentials: true }
|
||||
));
|
||||
}
|
||||
|
||||
private validateSession(): Observable<User> {
|
||||
return this.http.get<User>(
|
||||
this.BACKEND_PATH + '/session/validate',
|
||||
{ withCredentials: true }
|
||||
).pipe(
|
||||
first()
|
||||
);
|
||||
}
|
||||
|
||||
private destroySessions() {
|
||||
return this.http.post(
|
||||
this.BACKEND_PATH + '/session/destroy',
|
||||
{},
|
||||
{ withCredentials: true }
|
||||
).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
83
src/app/shared/components/popup/popup.component.css
Normal file
83
src/app/shared/components/popup/popup.component.css
Normal file
@@ -0,0 +1,83 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.popup-background {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
overflow-y: hidden;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.popup {
|
||||
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #dddd;
|
||||
border-radius: 18px;
|
||||
height: fit-content;
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
overflow: auto;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popup-close-btn {
|
||||
margin: 5px 20px;
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
.popup-close-btn::before,
|
||||
.popup-close-btn::after {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.271);
|
||||
background: #808080;
|
||||
border-radius: 5px;
|
||||
position: fixed;
|
||||
content: '';
|
||||
width: 25px;
|
||||
height: 5px;
|
||||
}
|
||||
.popup-close-btn::before {
|
||||
transform: translateY(150%) translateX(-10%) rotate(45deg);
|
||||
}
|
||||
.popup-close-btn::after {
|
||||
transform: translateY(150%) translateX(-10%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
height: calc(100% - 120px);
|
||||
padding: 0px 60px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
@media (min-width:767px) {
|
||||
|
||||
.popup {
|
||||
width: fit-content;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
}
|
||||
17
src/app/shared/components/popup/popup.component.html
Normal file
17
src/app/shared/components/popup/popup.component.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<div #popup
|
||||
class="popup-background"
|
||||
[@popupState]="popupState"
|
||||
(@popupState.done)="animationStop()">
|
||||
<div class="popup"
|
||||
appClickedOutside
|
||||
(clickOutside)="closePopup()"
|
||||
[ignoreElementList]="ignoreClickOutside">
|
||||
<div class="popup-header">
|
||||
<div class="popup-close-btn" (click)="closePopup()"></div>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="popup-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
23
src/app/shared/components/popup/popup.component.spec.ts
Normal file
23
src/app/shared/components/popup/popup.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PopupComponent } from './popup.component';
|
||||
|
||||
describe('PopupComponent', () => {
|
||||
let component: PopupComponent;
|
||||
let fixture: ComponentFixture<PopupComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ PopupComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PopupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
69
src/app/shared/components/popup/popup.component.ts
Normal file
69
src/app/shared/components/popup/popup.component.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { animate, animateChild, group, query, state, style, transition, trigger } from '@angular/animations';
|
||||
import { Component, ElementRef, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-popup',
|
||||
templateUrl: './popup.component.html',
|
||||
styleUrls: ['./popup.component.css'],
|
||||
animations: [
|
||||
trigger('popupState', [
|
||||
state('hide', style({
|
||||
'opacity': '0'
|
||||
})),
|
||||
state('show', style({
|
||||
'opacity': '1'
|
||||
})),
|
||||
transition(
|
||||
'* => show',
|
||||
group([
|
||||
query(
|
||||
"@*",
|
||||
animateChild(),
|
||||
{ optional: true }
|
||||
),
|
||||
animate('125ms ease-in')
|
||||
])
|
||||
),
|
||||
transition(
|
||||
'show => hide',
|
||||
group([
|
||||
query(
|
||||
"@*",
|
||||
animateChild(),
|
||||
{ optional: true }
|
||||
),
|
||||
animate('250ms ease-out')
|
||||
])
|
||||
)
|
||||
])
|
||||
]
|
||||
})
|
||||
export class PopupComponent {
|
||||
|
||||
@Input()
|
||||
state: boolean = false;
|
||||
|
||||
@Input()
|
||||
ignoreClickOutside!: HTMLDivElement[];
|
||||
|
||||
@Output()
|
||||
stateChange = new EventEmitter<boolean>(false);
|
||||
|
||||
constructor() { }
|
||||
|
||||
get popupState(): string {
|
||||
return this.state ? 'show' : 'hide';
|
||||
}
|
||||
|
||||
animationStop() {
|
||||
if (!this.state) {
|
||||
this.closePopup()
|
||||
this.stateChange.emit(false);
|
||||
}
|
||||
}
|
||||
|
||||
closePopup(): void {
|
||||
this.state = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SliderItemComponent } from './slider-item.component';
|
||||
|
||||
describe('SliderItemComponent', () => {
|
||||
let component: SliderItemComponent;
|
||||
let fixture: ComponentFixture<SliderItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ SliderItemComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SliderItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-slider-item',
|
||||
templateUrl: './slider-item.component.html',
|
||||
styleUrls: ['./slider-item.component.css'],
|
||||
animations:[
|
||||
trigger('animateSliderItem',[
|
||||
state('hide', style({
|
||||
'opacity': '0',
|
||||
'transform': 'translateX(150px)'
|
||||
}),
|
||||
{
|
||||
params: {
|
||||
fadeInTime: 600,
|
||||
fadeOutTime: 600
|
||||
}
|
||||
}),
|
||||
state('show', style({
|
||||
'opacity': '1',
|
||||
'transform': 'translateX(0px)'
|
||||
}),
|
||||
{
|
||||
params: {
|
||||
fadeOutTime: 600,
|
||||
fadeInTime: 600
|
||||
},
|
||||
}),
|
||||
transition('hide => show', animate(`{{ fadeInTime }}s ease-in`)),
|
||||
transition('show => hide', animate(`{{ fadeOutTime }}s ease-out`))
|
||||
])
|
||||
]
|
||||
})
|
||||
export class SliderItemComponent {
|
||||
|
||||
@Input()
|
||||
public state:boolean = false;
|
||||
|
||||
constructor() { }
|
||||
|
||||
get itemStatus(): string {
|
||||
return this.state ? 'show' : 'hide';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ClickedOutsideDirective } from './clicked-outside.directive';
|
||||
|
||||
describe('ClickedOutsideDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new ClickedOutsideDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { AfterViewInit, Directive, ElementRef, EventEmitter, Inject, Input, OnDestroy, Output, ViewChild } from '@angular/core';
|
||||
import { filter, fromEvent, Subscription } from 'rxjs';
|
||||
|
||||
@Directive({
|
||||
selector: '[appClickedOutside]'
|
||||
})
|
||||
export class ClickedOutsideDirective implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
ignoreElementList!: HTMLDivElement[];
|
||||
|
||||
@Input()
|
||||
includeClickedOutside!: HTMLDivElement[];
|
||||
|
||||
@Input()
|
||||
clickOutsideStopWatching: boolean = false;
|
||||
|
||||
@Output()
|
||||
clickOutside: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
clickListener!: Subscription;
|
||||
|
||||
constructor(
|
||||
private element: ElementRef,
|
||||
@Inject(DOCUMENT) private document: Document
|
||||
) { }
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
|
||||
|
||||
|
||||
this.clickListener = fromEvent(this.document, 'click')
|
||||
.pipe(
|
||||
filter((event) => {
|
||||
return !this.isInside(event.target as HTMLElement) || this.includedList(event.target as HTMLElement);
|
||||
})
|
||||
). subscribe( () => {
|
||||
!this.clickOutsideStopWatching && this.clickOutside.emit();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.clickListener?.unsubscribe();
|
||||
}
|
||||
|
||||
private isInside(elementToCheck: HTMLElement): boolean {
|
||||
return (
|
||||
elementToCheck === this.element.nativeElement
|
||||
|| this.element.nativeElement.contains(elementToCheck)
|
||||
|| (this.ignoreElementList && this.checkIgnoredList(elementToCheck))
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
private checkIgnoredList(elementToCheck: HTMLElement): boolean {
|
||||
return this.ignoreElementList.some(
|
||||
(ignoreElement) => {
|
||||
return ignoreElement === elementToCheck ||
|
||||
ignoreElement.contains(elementToCheck)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private includedList(elementToCheck: HTMLElement): boolean {
|
||||
return this.includeClickedOutside && this.includeClickedOutside.some(
|
||||
(includedElement) => {
|
||||
return includedElement === elementToCheck ||
|
||||
includedElement.contains(elementToCheck)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
18
src/app/shared/model/httpError/httpError.model-ti.ts
Normal file
18
src/app/shared/model/httpError/httpError.model-ti.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* This module was automatically generated by `ts-interface-builder`
|
||||
*/
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const HttpError = t.iface([], {
|
||||
"title": "string",
|
||||
"status": "number",
|
||||
"details": "string",
|
||||
"developerMessage": "string",
|
||||
"timestamp": "string",
|
||||
});
|
||||
|
||||
const HttpErrorTI: t.ITypeSuite = {
|
||||
HttpError,
|
||||
};
|
||||
export default HttpErrorTI;
|
||||
7
src/app/shared/model/httpError/httpError.model.ts
Normal file
7
src/app/shared/model/httpError/httpError.model.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface HttpError {
|
||||
title: string;
|
||||
status: number;
|
||||
details: string;
|
||||
developerMessage: string;
|
||||
timestamp: string;
|
||||
}
|
||||
5
src/app/shared/model/httpError/httpErrorChecker.ts
Normal file
5
src/app/shared/model/httpError/httpErrorChecker.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createCheckers } from "ts-interface-checker";
|
||||
import HttpErrorTI from "./httpError.model-ti";
|
||||
|
||||
const HttpErrorChecker = createCheckers(HttpErrorTI)['HttpError'];
|
||||
export default HttpErrorChecker;
|
||||
15
src/app/shared/model/token/token.model-ti.ts
Normal file
15
src/app/shared/model/token/token.model-ti.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* This module was automatically generated by `ts-interface-builder`
|
||||
*/
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const Token = t.iface([], {
|
||||
"token": "string",
|
||||
"expirationDate": t.union("string", "number"),
|
||||
});
|
||||
|
||||
const exportedTypeSuite: t.ITypeSuite = {
|
||||
Token,
|
||||
};
|
||||
export default exportedTypeSuite;
|
||||
4
src/app/shared/model/token/token.model.ts
Normal file
4
src/app/shared/model/token/token.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Token {
|
||||
token: string,
|
||||
expirationDate: string|number
|
||||
}
|
||||
6
src/app/shared/model/user/user.checker.ts
Normal file
6
src/app/shared/model/user/user.checker.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createCheckers } from "ts-interface-checker";
|
||||
import TokenTI from "../token/token.model-ti";
|
||||
import UserTI from "./user.model-ti";
|
||||
|
||||
const UserChecker = createCheckers(UserTI, TokenTI)['User'];
|
||||
export default UserChecker;
|
||||
23
src/app/shared/model/user/user.model-ti.ts
Normal file
23
src/app/shared/model/user/user.model-ti.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* This module was automatically generated by `ts-interface-builder`
|
||||
*/
|
||||
import * as t from "ts-interface-checker";
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
|
||||
export const User = t.iface([], {
|
||||
"id": t.opt("number"),
|
||||
"name": t.opt("string"),
|
||||
"email": t.opt("string"),
|
||||
"username": "string",
|
||||
"password": t.opt("string"),
|
||||
"accessToken": t.opt("Token"),
|
||||
"refreshToken": t.opt("Token"),
|
||||
"authorities": t.opt(t.array(t.iface([], {
|
||||
"authority": "string",
|
||||
}))),
|
||||
});
|
||||
|
||||
const UserTI: t.ITypeSuite = {
|
||||
User,
|
||||
};
|
||||
export default UserTI;
|
||||
13
src/app/shared/model/user/user.model.ts
Normal file
13
src/app/shared/model/user/user.model.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Token } from "../token/token.model";
|
||||
|
||||
export interface User {
|
||||
id?: number,
|
||||
fullname?: string,
|
||||
email?: string,
|
||||
username: string,
|
||||
password?: string,
|
||||
accessToken?: Token,
|
||||
refreshToken?: Token,
|
||||
authorities?: Array<{authority: string}>,
|
||||
validateAccessToken?: () => Token | undefined;
|
||||
};
|
||||
28
src/app/shared/shared.module.ts
Normal file
28
src/app/shared/shared.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ClickedOutsideDirective } from './directive/clicked-outside/clicked-outside.directive';
|
||||
import { SliderItemComponent } from './components/slider-item/slider-item.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { PopupComponent } from './components/popup/popup.component';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ClickedOutsideDirective,
|
||||
SliderItemComponent,
|
||||
PopupComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
FontAwesomeModule,
|
||||
],
|
||||
exports: [
|
||||
ClickedOutsideDirective,
|
||||
SliderItemComponent,
|
||||
PopupComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
Reference in New Issue
Block a user