diff --git a/deploy.php b/deploy.php new file mode 100644 index 0000000..42f2409 --- /dev/null +++ b/deploy.php @@ -0,0 +1,67 @@ +stage('staging') + ->user('yvan') + ->forwardAgent() + ->set('ng_basehref', '/admin/') + ->set('ng_target', 'production') + ->set('ng_environment', 'prod') + ->set('env_vars', 'NODE_ENV=production') + ->set('deploy_path', '/mnt/apps/granprize/admin'); + +host('granprize') + ->stage('production') + ->user('edvidan') + ->forwardAgent() + ->set('ng_basehref', '/admin/') + ->set('ng_target', 'production') + ->set('ng_environment', 'prod') + ->set('env_vars', 'NODE_ENV=production') + ->set('deploy_path', '/var/www/granprize.swedishchamber.hu/admin'); + +// Tasks +desc('Prepare release'); +task('deploy:ng-prepare', function() { + runLocally("ng build --base-href={{ng_basehref}} --target={{ng_target}} --environment={{ng_environment}}"); + runLocally("tar -cJf dist.tar.xz dist"); +}); + +desc('Upload release'); +task('deploy:ng-upload', function() { + upload("dist.tar.xz", "{{release_path}}/dist.tar.xz"); + run("tar -C {{release_path}} -xJf {{release_path}}/dist.tar.xz"); + run("rm -f {{release_path}}/dist.tar.xz"); + runLocally("rm -rf dist.tar.xz dist"); +}); + +desc('Deploy your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:lock', + 'deploy:release', + 'deploy:ng-prepare', + 'deploy:ng-upload', + 'deploy:shared', + 'deploy:clear_paths', + 'deploy:symlink', + 'deploy:unlock', + 'cleanup', +]); +after('deploy', 'success'); diff --git a/package-lock.json b/package-lock.json index 43f9474..e174d85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -392,6 +392,14 @@ "tslib": "1.9.0" } }, + "@auth0/angular-jwt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-2.0.0.tgz", + "integrity": "sha512-RVlXFpcqQ+9uCpzboU7Tm1ubaRVO2FrR5+RYuwHtTT4BXquVMEwOSbAuuaArFud/kMc00XYoGgiP1JkCfOAfpA==", + "requires": { + "url": "0.11.0" + } + }, "@fortawesome/fontawesome-free-webfonts": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.8.tgz", @@ -7547,8 +7555,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "querystring-es3": { "version": "0.2.1", @@ -9611,7 +9618,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, "requires": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -9620,8 +9626,7 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" } } }, diff --git a/package.json b/package.json index d133db3..866430a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@angular/platform-browser": "^6.0.0", "@angular/platform-browser-dynamic": "^6.0.0", "@angular/router": "^6.0.0", + "@auth0/angular-jwt": "^2.0.0", "@fortawesome/fontawesome-free-webfonts": "^1.0.8", "core-js": "^2.5.4", "rxjs": "^6.0.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index bc5bccc..42529c5 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -10,53 +10,61 @@ import { JudgeResolverService } from "./shared/judge-resolver.service"; import { AwardeeService } from "./shared/awardee.service"; import { AwardeeResolverService } from "./shared/awardee-resolver.service"; import { YearResolverService } from "./shared/year-resolver.service"; +import { AuthGuardService } from "./auth/auth-guard.service"; const routes: Routes = [ - { - path: 'awardees', - component: AwardeeListComponent, - resolve: { - awardees: AwardeeService, - } - // canActivate: [AuthGuardService, RoleGuardService], - }, { - path: 'awardee/new', - component: AwardeeEditorComponent, - resolve: { - years: YearResolverService, - } - // canActivate: [AuthGuardService, RoleGuardService], - }, { - path: 'awardee/edit/:id', - component: AwardeeEditorComponent, - resolve: { - awardee: AwardeeResolverService, - years: YearResolverService, - } - // canActivate: [AuthGuardService, RoleGuardService], - }, { - path: 'judges', - component: JudgeListComponent, - resolve: { - judges: JudgeService, - } - // canActivate: [AuthGuardService, RoleGuardService], - }, { - path: 'judge/new', - component: JudgeEditorComponent, - // canActivate: [AuthGuardService, RoleGuardService], - }, { - path: 'judge/edit/:id', - component: JudgeEditorComponent, - resolve: { - judge: JudgeResolverService, - } - // canActivate: [AuthGuardService, RoleGuardService], - } + { + path: 'awardees', + component: AwardeeListComponent, + resolve: { + awardees: AwardeeService, + }, + canActivate: [AuthGuardService], + }, { + path: 'awardee/new', + component: AwardeeEditorComponent, + resolve: { + years: YearResolverService, + }, + canActivate: [AuthGuardService], + }, { + path: 'awardee/edit/:id', + component: AwardeeEditorComponent, + resolve: { + awardee: AwardeeResolverService, + years: YearResolverService, + }, + canActivate: [AuthGuardService], + }, { + path: 'judges', + component: JudgeListComponent, + resolve: { + judges: JudgeService, + }, + canActivate: [AuthGuardService], + }, { + path: 'judge/new', + component: JudgeEditorComponent, + canActivate: [AuthGuardService], + }, { + path: 'judge/edit/:id', + component: JudgeEditorComponent, + resolve: { + judge: JudgeResolverService, + }, + canActivate: [AuthGuardService], + }, { + path: '', + redirectTo: '/awardees', + pathMatch: 'full', + canActivate: [AuthGuardService] + }, + // {path: '**', component: PageNotFoundComponent}, ]; @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] }) -export class AppRoutingModule {} +export class AppRoutingModule { +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e9f6ffa..b066d27 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,8 +1,15 @@ import { Component } from '@angular/core'; +import { AuthService } from "./auth/auth.service"; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent {} +export class AppComponent { + constructor(private authService: AuthService) {} + + get loggedIn(): boolean { + return this.authService.isLoggedIn; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c6ad88b..6d361ae 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -34,6 +34,7 @@ import { AwardeeListTableComponent } from './awardee-list-table/awardee-list-tab import { AwardeeEditorComponent } from './awardee-editor/awardee-editor.component'; import { JudgeEditorComponent } from './judge-editor/judge-editor.component'; import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; +import { AuthModule } from "./auth/auth.module"; @NgModule({ declarations: [ @@ -55,7 +56,6 @@ import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.componen BrowserAnimationsModule, FormsModule, HttpClientModule, - AppRoutingModule, LayoutModule, MatToolbarModule, MatButtonModule, @@ -74,6 +74,9 @@ import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.componen MatExpansionModule, MatTooltipModule, MatDialogModule, + + AuthModule, + AppRoutingModule, ], providers: [], bootstrap: [AppComponent] diff --git a/src/app/auth/auth-guard.service.spec.ts b/src/app/auth/auth-guard.service.spec.ts new file mode 100644 index 0000000..226d720 --- /dev/null +++ b/src/app/auth/auth-guard.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { AuthGuardService } from './auth-guard.service'; + +describe('AuthGuardService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AuthGuardService] + }); + }); + + it('should be created', inject([AuthGuardService], (service: AuthGuardService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/auth/auth-guard.service.ts b/src/app/auth/auth-guard.service.ts new file mode 100644 index 0000000..3e0f46c --- /dev/null +++ b/src/app/auth/auth-guard.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router"; + +import { AuthService } from "./auth.service"; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuardService { + constructor(private authService: AuthService, + private router: Router) { + } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + let url: string = state.url; + return this.checkLogin(url); + } + + checkLogin(url: string): boolean { + if (this.authService.isLoggedIn) { + return true; + } + + // Store the attempted URL for redirecting + this.authService.redirectUrl = url; + + // Navigate to the login page with extras + this.router.navigate(['/login']); + return false; + } +} diff --git a/src/app/auth/auth-routing.module.ts b/src/app/auth/auth-routing.module.ts new file mode 100644 index 0000000..d711e21 --- /dev/null +++ b/src/app/auth/auth-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { AuthComponent } from "./auth/auth.component"; + +const routes: Routes = [ + {path: 'login', component: AuthComponent}, + {path: 'logout', component: AuthComponent}, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AuthRoutingModule { } diff --git a/src/app/auth/auth.module.spec.ts b/src/app/auth/auth.module.spec.ts new file mode 100644 index 0000000..b39073a --- /dev/null +++ b/src/app/auth/auth.module.spec.ts @@ -0,0 +1,13 @@ +import { AuthModule } from './auth.module'; + +describe('AuthModule', () => { + let authModule: AuthModule; + + beforeEach(() => { + authModule = new AuthModule(); + }); + + it('should create an instance', () => { + expect(authModule).toBeTruthy(); + }); +}); diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts new file mode 100644 index 0000000..5aba8fa --- /dev/null +++ b/src/app/auth/auth.module.ts @@ -0,0 +1,43 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from "@angular/forms"; +import { JwtModule } from '@auth0/angular-jwt'; + +import { AuthRoutingModule } from './auth-routing.module'; +import { AuthComponent } from './auth/auth.component'; +import { + MatButtonModule, + MatCardModule, + MatFormFieldModule, + MatInputModule +} from "@angular/material"; + +export function tokenGetterFunctionWrapper() { + return localStorage.getItem('token'); +} + +@NgModule({ + imports: [ + JwtModule.forRoot({ + config: { + tokenGetter: tokenGetterFunctionWrapper, + whitelistedDomains: [ + "localhost:8888", + "granprize.dev.yvan.hu", + "granprize.swedishchamber.hu", + ], + }, + }), + CommonModule, + FormsModule, + AuthRoutingModule, + + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + ], + declarations: [AuthComponent] +}) +export class AuthModule { +} diff --git a/src/app/auth/auth.service.spec.ts b/src/app/auth/auth.service.spec.ts new file mode 100644 index 0000000..bd98634 --- /dev/null +++ b/src/app/auth/auth.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AuthService] + }); + }); + + it('should be created', inject([AuthService], (service: AuthService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts new file mode 100644 index 0000000..1d4c73e --- /dev/null +++ b/src/app/auth/auth.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core'; +import { environment } from "../../environments/environment"; +import { HttpClient } from "@angular/common/http"; +import { Router } from "@angular/router"; +import { JwtHelperService } from "@auth0/angular-jwt"; + +const TOKEN_LS_NAME = 'token'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private url = environment.apiUrl + '/auth'; + + public redirectUrl: string; + public isLoading: boolean = false; + public hasError: boolean = false; + public errorMessage: string = ""; + + constructor(private httpService: HttpClient, + private jwtHelperService: JwtHelperService, + private router: Router) { + } + + get token(): string { + return localStorage.getItem(TOKEN_LS_NAME); + } + + get isLoggedIn(): boolean { + try { + return !this.jwtHelperService.isTokenExpired(this.token); + } catch (ex) { + return false; + } + } + + get tokenData() { + return this.jwtHelperService.decodeToken(this.token); + } + + public authRedirect() { + this.router.navigate(['/login']); + } + + public login(login: string, password: string) { + this.hasError = false; + this.isLoading = true; + this.httpService.post(this.url + '/login',{ + 'user': login, + 'pass': password, + }).subscribe( + apiResponse => { + this.isLoading = false; + localStorage.setItem(TOKEN_LS_NAME, apiResponse); + this.router.navigate(['/']); + }, + err => { + console.log(err); + this.hasError = true; + this.errorMessage = "Hiba történt bejelentkezés közben."; + this.isLoading = false; + } + ); + } + + public renew() { + this.httpService.get(this.url + '/renew') + .subscribe( + apiResponse => { + localStorage.setItem(TOKEN_LS_NAME, apiResponse); + }, + err => console.log(err) + ); + } + + public logout() { + localStorage.removeItem(TOKEN_LS_NAME); + } +} diff --git a/src/app/auth/auth/auth.component.css b/src/app/auth/auth/auth.component.css new file mode 100644 index 0000000..daf438c --- /dev/null +++ b/src/app/auth/auth/auth.component.css @@ -0,0 +1,9 @@ +.mat-card { + min-width: 250px; + width: 25%; + margin: 150px auto auto; +} + +.mat-form-field { + width: 100%; +} \ No newline at end of file diff --git a/src/app/auth/auth/auth.component.html b/src/app/auth/auth/auth.component.html new file mode 100644 index 0000000..126e9e3 --- /dev/null +++ b/src/app/auth/auth/auth.component.html @@ -0,0 +1,12 @@ + +

GranPrize login

+ +
diff --git a/src/app/auth/auth/auth.component.spec.ts b/src/app/auth/auth/auth.component.spec.ts new file mode 100644 index 0000000..884576c --- /dev/null +++ b/src/app/auth/auth/auth.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AuthComponent } from './auth.component'; + +describe('AuthComponent', () => { + let component: AuthComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AuthComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AuthComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/auth/auth/auth.component.ts b/src/app/auth/auth/auth.component.ts new file mode 100644 index 0000000..419475d --- /dev/null +++ b/src/app/auth/auth/auth.component.ts @@ -0,0 +1,60 @@ +import { Component, OnInit } from '@angular/core'; +import { AuthService } from "../auth.service"; +import { Router } from "@angular/router"; + +@Component({ + selector: 'app-auth', + templateUrl: './auth.component.html', + styleUrls: ['./auth.component.css'] +}) +export class AuthComponent implements OnInit { + + public userName: string = ''; + public password: string = ''; + + constructor( + private authService: AuthService, + private router: Router + ) { + switch (this.router.url) { + case '/logout': + this.authService.logout(); + this.authService.authRedirect(); + return; + } + } + + ngOnInit() { + if (this.authService.isLoggedIn) { + this.router.navigate(['/']); + } + } + + get canLogin(): boolean { + return [ + this.userName, + this.password, + ].every(field => field.trim().length > 0); + } + + public doSubmit() { + if (this.canLogin) { + this.authService.login( + this.userName.trim(), + this.password.trim() + ); + } + } + + get hasError(): boolean { + return this.authService.hasError; + } + + get isLoading(): boolean { + return this.authService.isLoading; + } + + get errorMessage(): string { + return this.authService.errorMessage; + } +} diff --git a/src/app/awardee-list-table/awardee-list-table.component.html b/src/app/awardee-list-table/awardee-list-table.component.html index 050e493..0d25953 100644 --- a/src/app/awardee-list-table/awardee-list-table.component.html +++ b/src/app/awardee-list-table/awardee-list-table.component.html @@ -33,7 +33,7 @@ + [pageSize]="15" + [pageSizeOptions]="[15, 25, 50]"> - \ No newline at end of file + diff --git a/src/app/navigation/navigation.component.css b/src/app/navigation/navigation.component.css index acf02ca..4e8ee87 100644 --- a/src/app/navigation/navigation.component.css +++ b/src/app/navigation/navigation.component.css @@ -6,3 +6,7 @@ width: 200px; box-shadow: 3px 0 6px rgba(0,0,0,.24); } + +.nav-logout { + margin-top: 3em; +} diff --git a/src/app/navigation/navigation.component.html b/src/app/navigation/navigation.component.html index 4797154..06144de 100644 --- a/src/app/navigation/navigation.component.html +++ b/src/app/navigation/navigation.component.html @@ -1,5 +1,5 @@ - Menu - Judges - Awardees + + + Judges + + + + Awardees + + + + Logout + - +