diff --git a/customer/app/angular.json b/customer/app/angular.json index f824b83..dab6ea7 100644 --- a/customer/app/angular.json +++ b/customer/app/angular.json @@ -28,9 +28,11 @@ "src/assets" ], "styles": [ + "./node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss" ], - "scripts": [] + "scripts": [ + ] }, "configurations": { "production": { @@ -121,4 +123,4 @@ } }}, "defaultProject": "app" -} \ No newline at end of file +} diff --git a/customer/app/package-lock.json b/customer/app/package-lock.json index 4fc1a96..14c714b 100644 --- a/customer/app/package-lock.json +++ b/customer/app/package-lock.json @@ -498,14 +498,6 @@ } } }, - "@ng-bootstrap/ng-bootstrap": { - "version": "5.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-5.0.0-rc.1.tgz", - "integrity": "sha512-LRbA+qCbsY3rnXQiFbDG6M0fyDOi6n4MadUUrrnI6DyNAsH+uAg2twxkjXhkizUD6+XihpvR6AMsQ91T3Bndaw==", - "requires": { - "tslib": "^1.9.0" - } - }, "@ngtools/webpack": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.0.6.tgz", @@ -1533,6 +1525,11 @@ "multicast-dns-service-types": "^1.1.0" } }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5991,6 +5988,11 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "ngx-bootstrap": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-5.0.0.tgz", + "integrity": "sha512-5TTFP9s3wfiRychGcdyvpCvvxtxW1Nf2Dqmk2YBzuIhHHLT6gRq1Fsic4lYrtAUwmy0PSLhOY/XW/saYKlrSJw==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/customer/app/package.json b/customer/app/package.json index 10f7caf..f1e42f9 100644 --- a/customer/app/package.json +++ b/customer/app/package.json @@ -19,7 +19,8 @@ "@angular/platform-browser": "~8.0.3", "@angular/platform-browser-dynamic": "~8.0.3", "@angular/router": "~8.0.3", - "@ng-bootstrap/ng-bootstrap": "^5.0.0-rc.1", + "bootstrap": "^4.3.1", + "ngx-bootstrap": "^5.0.0", "rxjs": "~6.4.0", "tslib": "^1.9.0", "zone.js": "~0.9.1" diff --git a/customer/app/src/app/_guards/auth.guard.ts b/customer/app/src/app/_guards/auth.guard.ts new file mode 100644 index 0000000..598e472 --- /dev/null +++ b/customer/app/src/app/_guards/auth.guard.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@angular/core"; +import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; + +/* +The auth guard is used to prevent unauthenticated users from accessing restricted routes. +The auth guard will return: +TRUE: If the user is logged in and is authenticated to access the route +FALSE: If the user is logged out, thus not authenticated to access the route + +Here the route access condition is to be logged in (it works on the presence of a valid JWT token) +There can be other conditions too, like role based authentication + */ + + @Injectable({providedIn: 'root'}) + export class AuthGuard implements CanActivate{ + constructor(private router: Router){} + canActivate(router: ActivatedRouteSnapshot, state: RouterStateSnapshot){ + // check if the user is logged in + if(localStorage.getItem('currentUser')){ + return true; + } + + // not logged in so redirect to login page with the return url + this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }}); + return false; + } + + } diff --git a/customer/app/src/app/_guards/index.ts b/customer/app/src/app/_guards/index.ts new file mode 100644 index 0000000..3e48800 --- /dev/null +++ b/customer/app/src/app/_guards/index.ts @@ -0,0 +1 @@ +export * from './auth.guard'; \ No newline at end of file diff --git a/customer/app/src/app/_helpers/error.interceptor.ts b/customer/app/src/app/_helpers/error.interceptor.ts new file mode 100644 index 0000000..1660dfe --- /dev/null +++ b/customer/app/src/app/_helpers/error.interceptor.ts @@ -0,0 +1,36 @@ +import { Injectable } from "@angular/core"; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from "@angular/common/http"; +import { Observable, throwError } from "rxjs"; +import { catchError } from "rxjs/operators"; +import { AuthenticationService} from "../services/authentication.service"; + +/* +Http error interceptor works with the calling service and the API's +It intercepts the responses from the API and check for the status codes (if there were any errors). +Error Status 401: Unauthorized Response - the user will be automatically logged out +All other errors are RE-THROWN to be caught by the calling service so an alert can be displayed to the user + */ + + @Injectable() + export class ErrorInterceptor implements HttpInterceptor{ + constructor(private authenticationService: AuthenticationService){} + + intercept(request: HttpRequest, next: HttpHandler): Observable>{ + return next.handle(request) + .pipe( + catchError(err => { + if(err.status === 401){ + // auto logout on unauthorized response + this.authenticationService.logout(); + location.reload(true); + } + + const error = err.error.message || err.statusText; + return throwError(error); + + }) + ) + } + +} + diff --git a/customer/app/src/app/_helpers/fake-backend.ts b/customer/app/src/app/_helpers/fake-backend.ts new file mode 100644 index 0000000..b02b267 --- /dev/null +++ b/customer/app/src/app/_helpers/fake-backend.ts @@ -0,0 +1,110 @@ +import { Injectable } from "@angular/core"; +import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';; + +/* +FAKE BACKEND + +This will serve as a standalone backend with delayed response so that it can imitate a real backend - using +DELAYED OBSERVABLE + +1. It will check the user credentails that come from "Authentication Service" during login +2. It will also work as a fake database keeping the user details - The user can requests for the user details only when the requests has valid JWT Token in its request authorization header + +API +1. To check credentials - /users/authenticate +2. To give back user details - /users +*/ + + +@Injectable() +export class FakeBackendInterceptor implements HttpInterceptor{ + + constructor(){} + + intercept(request: HttpRequest, next: HttpHandler): Observable>{ + // test user - (one of the users detais in database) + let testUser = { + id: 1, + username: 'test', + password: 'test', + firstName: 'Test', + lastName: 'User' + } + + // wrapping the API's in delayed observable to simulate the Server API Calls + return of(null).pipe( + mergeMap(() => { + + // API 1: Authenticate - Will be hit by Authentication Service - STARTS + if(request.url.endsWith('/users/authenticate') && request.method === 'POST'){ + + + //check the credentials entered by the user with the data in database + if(request.body.username === testUser.username && request.body.password === testUser.password){ + + // if login details are valid, return status 200 with a Fake JWT Token + let body = { + id: testUser.id, + username: testUser.username, + firstName: testUser.firstName, + lastName: testUser.lastName, + token: '0000-fake-jwt-token-0000' + } + + return of(new HttpResponse({status: 200, body})) + } + else { + // if the credentials by user doesn't match the data in the db, return Status 400 - Bad Request + return throwError({ + error: { + message: 'Username or Password is incorrect.' + } + }) + } + } + // API 1: Authenticate - Will be hit by Authentication Service - ENDS + + + + // SECURE API END POINT - will check for valid JWT Token in Request + // API 2: Get all users data (we now have only 1 user - testUser) - STARTS + if(request.url.endsWith('/users') && request.method === 'GET'){ + + // check for a fake jwt token. If valid JWT token found, return the list of users, else throw error + if(request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000'){ + return of(new HttpResponse({status: 200, body: [testUser]})); + } + else{ + // invalid JWT token found in request header + return throwError({ + error: { + message: 'Unauthorized' + } + }); + } + } + // API 2: Get all users data (we now have only 1 user - testUser) - ENDS + + + // Pass any other requests left (unhandled + return next.handle(request); + }) + ) + // call materialize and dematerialize to ensure delay even if an error is thrown + .pipe(materialize()) + .pipe(delay(500)) + .pipe(dematerialize()); + } +} + +// creating a PROVIDER +export let fakeBackendProvider = { + // use fake backend in place of Http service for backend-less development + provide: HTTP_INTERCEPTORS, + useClass: FakeBackendInterceptor, + multi: true +}; + + diff --git a/customer/app/src/app/_helpers/index.ts b/customer/app/src/app/_helpers/index.ts new file mode 100644 index 0000000..b4668ef --- /dev/null +++ b/customer/app/src/app/_helpers/index.ts @@ -0,0 +1,3 @@ +export * from './error.interceptor'; +export * from './fake-backend'; +export * from './jwt.interceptor'; \ No newline at end of file diff --git a/customer/app/src/app/_helpers/jwt.interceptor.ts b/customer/app/src/app/_helpers/jwt.interceptor.ts new file mode 100644 index 0000000..f21380d --- /dev/null +++ b/customer/app/src/app/_helpers/jwt.interceptor.ts @@ -0,0 +1,31 @@ +import { Injectable } from "@angular/core"; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from "@angular/common/http"; +import { Observable } from "rxjs"; + +/* +The JWT interceptor intercepts the incoming requests from the application/user and adds JWT token to the request's Authorization header, only if the user is logged in. + +This JWT token in the request header is required to access the SECURE END API POINTS on the server +*/ + +@Injectable() +export class JwtInterceptor implements HttpInterceptor{ + constructor(){} + + intercept(request: HttpRequest, next: HttpHandler): Observable>{ + // check if the current user is logged in + // if the user making the request is logged in, he will have JWT token in it's local storage, which is set by Authorization Service during login process + let currentUser = JSON.parse(localStorage.getItem('currentUser')); + if(currentUser && currentUser.token){ + // clone the incoming request and add JWT token in the cloned request's Authorization Header + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${currentUser.token}` + } + }); + } + + // handle any other requests which went unhandled + return next.handle(request); + } +} \ No newline at end of file diff --git a/customer/app/src/app/app-routing.module.ts b/customer/app/src/app/app-routing.module.ts index d425c6f..e193ea1 100644 --- a/customer/app/src/app/app-routing.module.ts +++ b/customer/app/src/app/app-routing.module.ts @@ -1,7 +1,19 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import {HomeComponent} from "./pages/home/home.component"; +import {LoginComponent} from "./pages/login/login.component"; +import {ProfileComponent} from "./pages/profile/profile.component"; +import {AuthGuard} from "./_guards"; +import {EventsComponent} from "./pages/events/events.component"; -const routes: Routes = []; +const routes: Routes = [ + { path: 'home', component: HomeComponent }, + { path: '', redirectTo: '/home' , pathMatch: 'full'}, + { path: 'login', component: LoginComponent }, + { path: 'logout', redirectTo: '/login' , pathMatch: 'full'}, + { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] }, + { path: 'events', component: EventsComponent, canActivate: [AuthGuard] }, +]; @NgModule({ imports: [RouterModule.forRoot(routes)], diff --git a/customer/app/src/app/app.component.html b/customer/app/src/app/app.component.html index 0f3d9d8..7d3ea06 100644 --- a/customer/app/src/app/app.component.html +++ b/customer/app/src/app/app.component.html @@ -1,21 +1,3 @@ - -
-

- Welcome to {{ title }}! -

- Angular Logo -
-

Here are some links to help you start:

- - + + diff --git a/customer/app/src/app/app.module.ts b/customer/app/src/app/app.module.ts index 779829d..cb388f8 100644 --- a/customer/app/src/app/app.module.ts +++ b/customer/app/src/app/app.module.ts @@ -3,18 +3,46 @@ import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; +import { FitNavigationComponent } from './components/fit-navigation/fit-navigation.component'; +import { FitSlidesComponent } from './components/fit-slides/fit-slides.component'; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {CollapseModule} from "ngx-bootstrap"; +import { HomeComponent } from './pages/home/home.component'; +import { LoginComponent } from './pages/login/login.component'; +import { ProfileComponent } from './pages/profile/profile.component'; +import { JwtInterceptor, ErrorInterceptor } from './_helpers'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { fakeBackendProvider } from './_helpers'; +import {ReactiveFormsModule} from "@angular/forms"; +import { EventsComponent } from './pages/events/events.component'; + @NgModule({ declarations: [ - AppComponent + AppComponent, + FitNavigationComponent, + FitSlidesComponent, + HomeComponent, + LoginComponent, + ProfileComponent, + EventsComponent, + ], imports: [ BrowserModule, AppRoutingModule, - NgbModule + ReactiveFormsModule, + BrowserAnimationsModule, + CollapseModule.forRoot(), + HttpClientModule + ], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, + + // provider used to create fake backend + fakeBackendProvider ], - providers: [], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/customer/app/src/app/components/fit-navigation/fit-navigation.component.html b/customer/app/src/app/components/fit-navigation/fit-navigation.component.html new file mode 100644 index 0000000..8b54512 --- /dev/null +++ b/customer/app/src/app/components/fit-navigation/fit-navigation.component.html @@ -0,0 +1,19 @@ + diff --git a/customer/app/src/app/components/fit-navigation/fit-navigation.component.scss b/customer/app/src/app/components/fit-navigation/fit-navigation.component.scss new file mode 100644 index 0000000..4d31ddc --- /dev/null +++ b/customer/app/src/app/components/fit-navigation/fit-navigation.component.scss @@ -0,0 +1,122 @@ +.fit-navbar { + background: transparent !important; +} + +.fit-navbar-absolute { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 3; +} + + +@media (max-width: 991.98px) { + .fit-navbar { + background: #000 !important; + position: relative; + top: 0; + } +} + +.fit-navbar .navbar-brand { + color: #fff; +} + + +.fit-navbar .navbar-nav > .nav-item > .nav-link { + font-size: 13px; + padding: .7rem 0; + color: rgba(255, 255, 255, 0.9); + font-weight: 400; + position: relative; + text-transform: uppercase; + letter-spacing: 2px; + opacity: 1 !important; +} + +@media (min-width: 768px) { + .fit-navbar .navbar-nav > .nav-item > .nav-link { + padding: 1.5rem 20px; + color: rgba(255, 255, 255, 0.9); + } +} + +.fit-navbar .navbar-nav > .nav-item > .nav-link:hover, .fit-navbar .navbar-nav > .nav-item > .nav-link:focus { + color: #e5ce48; +} + +.fit-navbar .navbar-nav > .nav-item.active > a { + color: #e5ce48; +} + +.fit-navbar .navbar-nav > .nav-item.active > a:after { + opacity: 1; + background: #e5ce48; +} + +.fit-navbar .navbar-nav > .nav-item > .nav-link:hover:after, .fit-navbar .navbar-nav > .nav-item > .nav-link:focus:after { + opacity: 1; +} + +.fit-navbar .navbar-nav > .nav-item .dropdown-menu { + border: none; + background: #000; + border-radius: 0; + -webkit-box-shadow: 0px 10px 34px -20px rgba(0, 0, 0, 0.41); + -moz-box-shadow: 0px 10px 34px -20px rgba(0, 0, 0, 0.41); + box-shadow: 0px 10px 34px -20px rgba(0, 0, 0, 0.41); +} + +.fit-navbar .navbar-nav > .nav-item .dropdown-menu .dropdown-item { + font-size: 13px; + color: rgba(255, 255, 255, 0.5); + background: #0d0d0d; +} + +.fit-navbar .navbar-nav > .nav-item .dropdown-menu .dropdown-item:hover, .fit-navbar .navbar-nav > .nav-item .dropdown-menu .dropdown-item:focus { + color: #fff; +} + + + +.fit-navbar .navbar-nav > .nav-item.active > a { + color: #e5ce48; +} + +.fit-navbar .navbar-nav > .nav-item.active > a:after { + opacity: 1; + background: #e5ce48; +} + +.fit-navbar .navbar-toggler { + border: none; + color: rgba(255, 255, 255, 0.8) !important; + cursor: pointer; + padding-right: 0; + text-transform: uppercase; + font-size: 16px; + letter-spacing: .1em; +} + +.fit-navbar .navbar-toggler:hover, .ftco-navbar-light .navbar-toggler:focus { + outline: none !important; +} + + + +.navbar-brand { + font-size: 22px; + line-height: 1; + text-transform: uppercase; + display: block; + font-weight: 900; +} + +.navbar-brand small { + text-transform: uppercase; + font-size: 12px; + display: block; + color: #fff; + letter-spacing: 8px; +} diff --git a/customer/app/src/app/components/fit-navigation/fit-navigation.component.spec.ts b/customer/app/src/app/components/fit-navigation/fit-navigation.component.spec.ts new file mode 100644 index 0000000..7205ced --- /dev/null +++ b/customer/app/src/app/components/fit-navigation/fit-navigation.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FitNavigationComponent } from './fit-navigation.component'; + +describe('FitNavigationComponent', () => { + let component: FitNavigationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ FitNavigationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FitNavigationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/customer/app/src/app/components/fit-navigation/fit-navigation.component.ts b/customer/app/src/app/components/fit-navigation/fit-navigation.component.ts new file mode 100644 index 0000000..f6c21a8 --- /dev/null +++ b/customer/app/src/app/components/fit-navigation/fit-navigation.component.ts @@ -0,0 +1,81 @@ +import { Component, OnInit } from '@angular/core'; +import {Router} from "@angular/router"; +import {AuthenticationService} from "../../services/authentication.service"; + +@Component({ + selector: 'fit-navigation', + templateUrl: './fit-navigation.component.html', + styleUrls: ['./fit-navigation.component.scss'] +}) +export class FitNavigationComponent implements OnInit { + + isCollapsed = true; + items: MenuItem[] = [ + { + route: '/home', + label: 'Home', + roles: ['*'] + }, + { + route: '/profile', + label: 'Profil', + roles: ['@'] + }, + { + route: '/events', + label: 'Események', + roles: ['@'] + }, + { + route: '/login', + label: 'Bejelentkezés', + roles: ['!'] + }, + { + route: '/logout', + label: 'Kijelentkezés', + roles: ['@'] + } + ]; + + constructor(private router: Router , private authenticationService: AuthenticationService) { } + + ngOnInit() { + } + + /** + * Return true, if content css position must be absolute . + * If content css is absolute, it means, that the content occupies the whole page. + * If not absolute, it means, that the content starts after the menubar. + */ + isAbsolute( ){ + return '/home' == this.router.url; + } + + isVisible(item: MenuItem){ + if ( item.roles && item.roles.length ){ + let firstRole = item.roles[0]; + if ( firstRole == '!'){ + return !this.authenticationService.user.value; + }else if ( firstRole == '*'){ + return true; + }else if ( firstRole == '@'){ + return this.authenticationService.user.value; + } + } + + return true; + } + +} + +export interface MenuItem { + route: string; + label: string; + /** + * ['*'] -> guests + * ['!'] -> only guests + * ['@'] -> authenticated + */ + roles: string[]; +} diff --git a/customer/app/src/app/components/fit-slides/fit-slides.component.html b/customer/app/src/app/components/fit-slides/fit-slides.component.html new file mode 100644 index 0000000..15f5e18 --- /dev/null +++ b/customer/app/src/app/components/fit-slides/fit-slides.component.html @@ -0,0 +1,31 @@ + diff --git a/customer/app/src/app/components/fit-slides/fit-slides.component.scss b/customer/app/src/app/components/fit-slides/fit-slides.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/customer/app/src/app/components/fit-slides/fit-slides.component.spec.ts b/customer/app/src/app/components/fit-slides/fit-slides.component.spec.ts new file mode 100644 index 0000000..8607c08 --- /dev/null +++ b/customer/app/src/app/components/fit-slides/fit-slides.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FitSlidesComponent } from './fit-slides.component'; + +describe('FitSlidesComponent', () => { + let component: FitSlidesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ FitSlidesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FitSlidesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/customer/app/src/app/components/fit-slides/fit-slides.component.ts b/customer/app/src/app/components/fit-slides/fit-slides.component.ts new file mode 100644 index 0000000..e70f960 --- /dev/null +++ b/customer/app/src/app/components/fit-slides/fit-slides.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'fit-slides', + templateUrl: './fit-slides.component.html', + styleUrls: ['./fit-slides.component.scss'] +}) +export class FitSlidesComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/customer/app/src/app/pages/events/events.component.html b/customer/app/src/app/pages/events/events.component.html new file mode 100644 index 0000000..f2f77fd --- /dev/null +++ b/customer/app/src/app/pages/events/events.component.html @@ -0,0 +1 @@ +

events works!

diff --git a/customer/app/src/app/pages/events/events.component.scss b/customer/app/src/app/pages/events/events.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/customer/app/src/app/pages/events/events.component.spec.ts b/customer/app/src/app/pages/events/events.component.spec.ts new file mode 100644 index 0000000..a69136c --- /dev/null +++ b/customer/app/src/app/pages/events/events.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EventsComponent } from './events.component'; + +describe('EventsComponent', () => { + let component: EventsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ EventsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EventsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/customer/app/src/app/pages/events/events.component.ts b/customer/app/src/app/pages/events/events.component.ts new file mode 100644 index 0000000..9c877eb --- /dev/null +++ b/customer/app/src/app/pages/events/events.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-events', + templateUrl: './events.component.html', + styleUrls: ['./events.component.scss'] +}) +export class EventsComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/customer/app/src/app/pages/home/home.component.html b/customer/app/src/app/pages/home/home.component.html new file mode 100644 index 0000000..5f2c53f --- /dev/null +++ b/customer/app/src/app/pages/home/home.component.html @@ -0,0 +1 @@ +

home works!

diff --git a/customer/app/src/app/pages/home/home.component.scss b/customer/app/src/app/pages/home/home.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/customer/app/src/app/pages/home/home.component.spec.ts b/customer/app/src/app/pages/home/home.component.spec.ts new file mode 100644 index 0000000..490e81b --- /dev/null +++ b/customer/app/src/app/pages/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HomeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/customer/app/src/app/pages/home/home.component.ts b/customer/app/src/app/pages/home/home.component.ts new file mode 100644 index 0000000..f56c8c1 --- /dev/null +++ b/customer/app/src/app/pages/home/home.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/customer/app/src/app/pages/login/login.component.html b/customer/app/src/app/pages/login/login.component.html new file mode 100644 index 0000000..0a62245 --- /dev/null +++ b/customer/app/src/app/pages/login/login.component.html @@ -0,0 +1,28 @@ +
+
+
+ + +
+
+
diff --git a/customer/app/src/app/pages/login/login.component.scss b/customer/app/src/app/pages/login/login.component.scss new file mode 100644 index 0000000..19a9167 --- /dev/null +++ b/customer/app/src/app/pages/login/login.component.scss @@ -0,0 +1,8 @@ +//.btn-primary{ +// background-color: #e5ce48; +// color: orangered; +// border: 1px solid orangered; +//} +//.btn-primary:hover{ +// background-color: orange; +//} diff --git a/customer/app/src/app/pages/login/login.component.spec.ts b/customer/app/src/app/pages/login/login.component.spec.ts new file mode 100644 index 0000000..d6d85a8 --- /dev/null +++ b/customer/app/src/app/pages/login/login.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/customer/app/src/app/pages/login/login.component.ts b/customer/app/src/app/pages/login/login.component.ts new file mode 100644 index 0000000..0396591 --- /dev/null +++ b/customer/app/src/app/pages/login/login.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {ActivatedRoute, Router} from "@angular/router"; +import {AuthenticationService} from "../../services/authentication.service"; +import { first } from 'rxjs/operators'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent implements OnInit { + + loginForm: FormGroup; + submitted = false; + loading = false; + returnUrl: string; + error = ''; + + constructor(private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private authenticationService: AuthenticationService) { } + + ngOnInit() { + this.loginForm = this.formBuilder.group({ + username: ['', [Validators.required]], + password: ['', Validators.required] + }); + + // logout the person when he opens the app for the first time + this.authenticationService.logout(); + + // get return url from route parameters or default to '/' + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; + } + + + // convenience getter for easy access to form fields + get f(){ + return this.loginForm.controls; + } + + // on submit + onSubmit(){ + this.submitted = true; + + // stop if form is invalid + if(this.loginForm.invalid){ + return; + } + + this.loading = true; + + this.authenticationService.login(this.f.username.value, this.f.password.value) + .pipe(first()) + .subscribe( + data => { + this.router.navigate([this.returnUrl]); + }, + error => { + this.error = error; + this.loading = false; + } + ) + } + + get username(){ + return this.loginForm.get('username'); + } + +} diff --git a/customer/app/src/app/pages/profile/profile.component.html b/customer/app/src/app/pages/profile/profile.component.html new file mode 100644 index 0000000..9df0576 --- /dev/null +++ b/customer/app/src/app/pages/profile/profile.component.html @@ -0,0 +1 @@ +

profile works!

diff --git a/customer/app/src/app/pages/profile/profile.component.scss b/customer/app/src/app/pages/profile/profile.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/customer/app/src/app/pages/profile/profile.component.spec.ts b/customer/app/src/app/pages/profile/profile.component.spec.ts new file mode 100644 index 0000000..692b234 --- /dev/null +++ b/customer/app/src/app/pages/profile/profile.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfileComponent } from './profile.component'; + +describe('ProfileComponent', () => { + let component: ProfileComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ProfileComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/customer/app/src/app/pages/profile/profile.component.ts b/customer/app/src/app/pages/profile/profile.component.ts new file mode 100644 index 0000000..a9b65fc --- /dev/null +++ b/customer/app/src/app/pages/profile/profile.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.scss'] +}) +export class ProfileComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/customer/app/src/app/services/authentication.service.spec.ts b/customer/app/src/app/services/authentication.service.spec.ts new file mode 100644 index 0000000..91a1e97 --- /dev/null +++ b/customer/app/src/app/services/authentication.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthenticationService } from './authentication.service'; + +describe('AuthenticationService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: AuthenticationService = TestBed.get(AuthenticationService); + expect(service).toBeTruthy(); + }); +}); diff --git a/customer/app/src/app/services/authentication.service.ts b/customer/app/src/app/services/authentication.service.ts new file mode 100644 index 0000000..2f9aadc --- /dev/null +++ b/customer/app/src/app/services/authentication.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from "@angular/common/http"; +import { map } from 'rxjs/operators'; +import {Endpoints} from "./endpoints"; +import {BehaviorSubject} from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationService { + private _user: BehaviorSubject = new BehaviorSubject(null); + constructor(private http: HttpClient){} + + // login + login(username: string, password:string){ + return this.http.post(Endpoints.POST_USERS_AUTHENTICATE(), {username,password}) + .pipe( + // the backend service sends an instance of the user + // user: any (because .post) + map(user => { + // login successful if the response has jwt token + if(user && user.token){ + // store user details and jwt token in the local storage to keep the user logged in between page refreshes + localStorage.setItem('currentUser', JSON.stringify(user)); + this.user.next(user); + } + + return user; + }) + ); + } + + get user() { + return this._user; + } + + // logout + logout(){ + // remove user from local storage + localStorage.removeItem('currentUser'); + this.user.next(null); + } +} diff --git a/customer/app/src/app/services/endpoints.ts b/customer/app/src/app/services/endpoints.ts new file mode 100644 index 0000000..0cede1a --- /dev/null +++ b/customer/app/src/app/services/endpoints.ts @@ -0,0 +1,9 @@ +export class Endpoints { + private static contextPath = "/api"; + private static baseUrl: string = Endpoints.contextPath + "/rest"; + + public static POST_USERS_AUTHENTICATE(){ + return `${this.baseUrl}/users/authenticate`; + } + +} diff --git a/customer/app/src/app/services/login.service.ts b/customer/app/src/app/services/login.service.ts new file mode 100644 index 0000000..c47ec69 --- /dev/null +++ b/customer/app/src/app/services/login.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LoginService { + + constructor() { + + } + + public login(){ + + } + + public logout(){ + + } + +} diff --git a/customer/app/src/assets/images/about.jpg b/customer/app/src/assets/images/about.jpg new file mode 100644 index 0000000..932592c Binary files /dev/null and b/customer/app/src/assets/images/about.jpg differ diff --git a/customer/app/src/assets/images/bg_1.jpg b/customer/app/src/assets/images/bg_1.jpg new file mode 100644 index 0000000..eb6462d Binary files /dev/null and b/customer/app/src/assets/images/bg_1.jpg differ diff --git a/customer/app/src/assets/images/bg_2.jpg b/customer/app/src/assets/images/bg_2.jpg new file mode 100644 index 0000000..0879ea9 Binary files /dev/null and b/customer/app/src/assets/images/bg_2.jpg differ diff --git a/customer/app/src/assets/images/bg_3.jpg b/customer/app/src/assets/images/bg_3.jpg new file mode 100644 index 0000000..b536530 Binary files /dev/null and b/customer/app/src/assets/images/bg_3.jpg differ diff --git a/customer/app/src/assets/images/bg_4.jpg b/customer/app/src/assets/images/bg_4.jpg new file mode 100644 index 0000000..e815f64 Binary files /dev/null and b/customer/app/src/assets/images/bg_4.jpg differ diff --git a/customer/app/src/assets/images/bg_4_1.jpg b/customer/app/src/assets/images/bg_4_1.jpg new file mode 100644 index 0000000..e12f709 Binary files /dev/null and b/customer/app/src/assets/images/bg_4_1.jpg differ diff --git a/customer/app/src/assets/images/bg_5.jpg b/customer/app/src/assets/images/bg_5.jpg new file mode 100644 index 0000000..1e34cbd Binary files /dev/null and b/customer/app/src/assets/images/bg_5.jpg differ diff --git a/customer/app/src/assets/images/gallery-1.jpg b/customer/app/src/assets/images/gallery-1.jpg new file mode 100644 index 0000000..467057c Binary files /dev/null and b/customer/app/src/assets/images/gallery-1.jpg differ diff --git a/customer/app/src/assets/images/gallery-2.jpg b/customer/app/src/assets/images/gallery-2.jpg new file mode 100644 index 0000000..0d63b4c Binary files /dev/null and b/customer/app/src/assets/images/gallery-2.jpg differ diff --git a/customer/app/src/assets/images/gallery-3.jpg b/customer/app/src/assets/images/gallery-3.jpg new file mode 100644 index 0000000..4308c15 Binary files /dev/null and b/customer/app/src/assets/images/gallery-3.jpg differ diff --git a/customer/app/src/assets/images/gallery-4.jpg b/customer/app/src/assets/images/gallery-4.jpg new file mode 100644 index 0000000..4e29a8f Binary files /dev/null and b/customer/app/src/assets/images/gallery-4.jpg differ diff --git a/customer/app/src/assets/images/image_1.jpg b/customer/app/src/assets/images/image_1.jpg new file mode 100644 index 0000000..b59a2e9 Binary files /dev/null and b/customer/app/src/assets/images/image_1.jpg differ diff --git a/customer/app/src/assets/images/image_2.jpg b/customer/app/src/assets/images/image_2.jpg new file mode 100644 index 0000000..deb36de Binary files /dev/null and b/customer/app/src/assets/images/image_2.jpg differ diff --git a/customer/app/src/assets/images/image_3.jpg b/customer/app/src/assets/images/image_3.jpg new file mode 100644 index 0000000..a5ed139 Binary files /dev/null and b/customer/app/src/assets/images/image_3.jpg differ diff --git a/customer/app/src/assets/images/image_4.jpg b/customer/app/src/assets/images/image_4.jpg new file mode 100644 index 0000000..5a895fa Binary files /dev/null and b/customer/app/src/assets/images/image_4.jpg differ diff --git a/customer/app/src/assets/images/image_5.jpg b/customer/app/src/assets/images/image_5.jpg new file mode 100644 index 0000000..ffcf78c Binary files /dev/null and b/customer/app/src/assets/images/image_5.jpg differ diff --git a/customer/app/src/assets/images/image_6.jpg b/customer/app/src/assets/images/image_6.jpg new file mode 100644 index 0000000..92b4eab Binary files /dev/null and b/customer/app/src/assets/images/image_6.jpg differ diff --git a/customer/app/src/assets/images/loc.png b/customer/app/src/assets/images/loc.png new file mode 100644 index 0000000..c9e2381 Binary files /dev/null and b/customer/app/src/assets/images/loc.png differ diff --git a/customer/app/src/assets/images/person_1.jpg b/customer/app/src/assets/images/person_1.jpg new file mode 100644 index 0000000..225dc94 Binary files /dev/null and b/customer/app/src/assets/images/person_1.jpg differ diff --git a/customer/app/src/assets/images/person_2.jpg b/customer/app/src/assets/images/person_2.jpg new file mode 100644 index 0000000..a8fa93d Binary files /dev/null and b/customer/app/src/assets/images/person_2.jpg differ diff --git a/customer/app/src/assets/images/person_3.jpg b/customer/app/src/assets/images/person_3.jpg new file mode 100644 index 0000000..573d9db Binary files /dev/null and b/customer/app/src/assets/images/person_3.jpg differ diff --git a/customer/app/src/assets/images/person_4.jpg b/customer/app/src/assets/images/person_4.jpg new file mode 100644 index 0000000..5d7966d Binary files /dev/null and b/customer/app/src/assets/images/person_4.jpg differ diff --git a/customer/app/src/assets/images/program-1.jpg b/customer/app/src/assets/images/program-1.jpg new file mode 100644 index 0000000..8166f52 Binary files /dev/null and b/customer/app/src/assets/images/program-1.jpg differ diff --git a/customer/app/src/assets/images/program-2.jpg b/customer/app/src/assets/images/program-2.jpg new file mode 100644 index 0000000..ee4a0d2 Binary files /dev/null and b/customer/app/src/assets/images/program-2.jpg differ diff --git a/customer/app/src/assets/images/program-3.jpg b/customer/app/src/assets/images/program-3.jpg new file mode 100644 index 0000000..6bd1983 Binary files /dev/null and b/customer/app/src/assets/images/program-3.jpg differ diff --git a/customer/app/src/assets/images/program-4.jpg b/customer/app/src/assets/images/program-4.jpg new file mode 100644 index 0000000..ea91873 Binary files /dev/null and b/customer/app/src/assets/images/program-4.jpg differ diff --git a/customer/app/src/assets/images/program-5.jpg b/customer/app/src/assets/images/program-5.jpg new file mode 100644 index 0000000..5624bfb Binary files /dev/null and b/customer/app/src/assets/images/program-5.jpg differ diff --git a/customer/app/src/assets/images/program-6.jpg b/customer/app/src/assets/images/program-6.jpg new file mode 100644 index 0000000..4a38e5e Binary files /dev/null and b/customer/app/src/assets/images/program-6.jpg differ diff --git a/customer/app/src/assets/images/trainer-1.jpg b/customer/app/src/assets/images/trainer-1.jpg new file mode 100644 index 0000000..a37831f Binary files /dev/null and b/customer/app/src/assets/images/trainer-1.jpg differ diff --git a/customer/app/src/assets/images/trainer-2.jpg b/customer/app/src/assets/images/trainer-2.jpg new file mode 100644 index 0000000..6a70da2 Binary files /dev/null and b/customer/app/src/assets/images/trainer-2.jpg differ diff --git a/customer/app/src/assets/images/trainer-3.jpg b/customer/app/src/assets/images/trainer-3.jpg new file mode 100644 index 0000000..bf860f9 Binary files /dev/null and b/customer/app/src/assets/images/trainer-3.jpg differ diff --git a/customer/app/src/assets/images/trainer-4.jpg b/customer/app/src/assets/images/trainer-4.jpg new file mode 100644 index 0000000..df80bab Binary files /dev/null and b/customer/app/src/assets/images/trainer-4.jpg differ diff --git a/customer/app/src/assets/images/trainer-5.jpg b/customer/app/src/assets/images/trainer-5.jpg new file mode 100644 index 0000000..f1c6f52 Binary files /dev/null and b/customer/app/src/assets/images/trainer-5.jpg differ diff --git a/customer/app/src/assets/images/trainer-6.jpg b/customer/app/src/assets/images/trainer-6.jpg new file mode 100644 index 0000000..ba90e6f Binary files /dev/null and b/customer/app/src/assets/images/trainer-6.jpg differ diff --git a/customer/app/src/index.html b/customer/app/src/index.html index 6cf2430..3f995a1 100644 --- a/customer/app/src/index.html +++ b/customer/app/src/index.html @@ -1,13 +1,16 @@ + Botond Fitness - App + + + - + diff --git a/customer/app/src/styles.scss b/customer/app/src/styles.scss index 90d4ee0..698e011 100644 --- a/customer/app/src/styles.scss +++ b/customer/app/src/styles.scss @@ -1 +1,110 @@ /* You can add global styles to this file, and also import other style files */ + + +body{ + font-family: "Poppins", Arial, sans-serif; + background: #151111; + font-size: 15px; + line-height: 1.8; + font-weight: 300; + color: gray; + background: url("/assets/images/bg_4.jpg") no-repeat fixed; + background-size: cover; +} + +.fit-content{ + + .fit-navbar{ + position: relative; + } + +} + + +.btn { + cursor: pointer; + -webkit-border-radius: 0; + -moz-border-radius: 0; + -ms-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none !important; + box-shadow: none !important; + font-size: 13px; } +.btn:hover, .btn:active, .btn:focus { + outline: none; } +.btn.btn-primary { + background: #e5ce48; + border: 1px solid #e5ce48; + color: #000; } +.btn.btn-primary:hover { + border: 1px solid #e5ce48; + background: transparent; + color: #e5ce48; } +.btn.btn-primary.btn-outline-primary { + border: 1px solid #e5ce48; + background: transparent; + color: #e5ce48; } +.btn.btn-primary.btn-outline-primary:hover { + border: 1px solid transparent; + background: #e5ce48; + color: #fff; } +.btn.btn-white { + background: #fff; + border: 1px solid #fff; + color: #000; } +.btn.btn-white:hover { + border: 1px solid #000; + background: #000; + color: #fff; } +.btn.btn-white.btn-outline-white { + border-color: rgba(255, 255, 255, 0.8); + background: none; + border-width: 1px; + color: #fff; } +.btn.btn-white.btn-outline-white:hover, .btn.btn-white.btn-outline-white:focus, .btn.btn-white.btn-outline-white:active { + background: #e5ce48; + border-color: #e5ce48; + color: #000; } +.btn.btn-outline-black { + border-color: black; + background: none; + border-width: 1px; + color: #000; } +.btn.btn-outline-black:hover, .btn.btn-outline-black:focus, .btn.btn-outline-black:active { + background: #000; + border-color: #000; + color: #fff; } + + +.login-form .form-group { + position: relative; } + +.login-form .form-control { + border: transparent !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important; + height: 58px !important; + padding-left: 0; + padding-right: 0; + background: transparent !important; + color: rgba(255, 255, 255, 0.9) !important; + font-size: 13px; + border-radius: 0px; + -webkit-box-shadow: none !important; + box-shadow: none !important; } +.login-form .form-control::-webkit-input-placeholder { + /* Chrome/Opera/Safari */ + color: rgba(255, 255, 255, 0.9); } +.login-form .form-control::-moz-placeholder { + /* Firefox 19+ */ + color: rgba(255, 255, 255, 0.9); } +.login-form .form-control:-ms-input-placeholder { + /* IE 10+ */ + color: rgba(255, 255, 255, 0.9); } +.login-form .form-control:-moz-placeholder { + /* Firefox 18- */ + color: rgba(255, 255, 255, 0.9); } +.login-form .form-control:focus, .login-form .form-control:active { + border-color: #e5ce48 !important; } + +.login-form textarea.form-control { + height: inherit !important; }