From b2b9d37f03440957d87417cb83df449bcc4705aa Mon Sep 17 00:00:00 2001 From: Turin Andrey Date: Tue, 1 Nov 2022 17:29:25 +0300 Subject: [PATCH 1/3] added auth module, login and signup pages --- package-lock.json | 11 +++ package.json | 1 + src/assets/config/dev.config.json | 3 + src/iap/layout/layout-routing.module.ts | 12 ++- src/iap/modules/auth/auth.module.ts | 31 ++++++ .../login-form/login-form.component.html | 41 ++++++++ .../login-form/login-form.component.scss | 54 ++++++++++ .../login-form/login-form.component.ts | 30 ++++++ .../login-page/login-page.component.html | 9 ++ .../login-page/login-page.component.scss | 32 ++++++ .../login-page/login-page.component.ts | 38 +++++++ .../signup-form/signup-form.component.html | 98 +++++++++++++++++++ .../signup-form/signup-form.component.scss | 38 +++++++ .../signup-form/signup-form.component.ts | 61 ++++++++++++ .../signup-page/signup-page.component.html | 12 +++ .../signup-page/signup-page.component.scss | 24 +++++ .../signup-page/signup-page.component.ts | 53 ++++++++++ .../interceptors/http-token.interceptor.ts | 23 +++++ .../modules/auth/services/jwt-auth.service.ts | 59 +++++++++++ .../dev-page/dev-page.component.html | 4 + .../dev-page/dev-page.component.scss | 6 +- .../components/dev-page/dev-page.component.ts | 2 +- src/iap/modules/dev/dev.module.ts | 3 +- src/iap/modules/modules.module.ts | 3 +- .../game-card/game-card.component.ts | 2 +- .../preview-slide/preview-slide.component.ts | 2 +- src/iap/shared/constants/auth.ts | 2 + src/iap/shared/constants/pages.ts | 6 ++ src/iap/shared/constants/tokens.ts | 2 + .../interfaces/auth-service.interface.ts | 11 +++ .../interfaces/user-service.interface.ts | 10 ++ src/iap/shared/models/auth.models.ts | 13 +++ src/iap/shared/models/dto/jwt-session.dto.ts | 4 + .../shared/models/dto/update-profile.dto.ts | 5 + .../game.model.ts => entities/game.entity.ts} | 0 .../shared/models/entities/profile.entity.ts | 10 ++ src/iap/shared/models/entities/user.entity.ts | 3 + src/iap/shared/modules/api/api.module.ts | 18 ++++ .../modules/api/services/api.service.ts | 44 +++++++++ .../modules/api/services/user.service.ts | 33 +++++++ src/iap/shared/services/config.service.ts | 23 +++++ src/iap/shared/shared.module.ts | 24 ++++- .../validators/is-login-unique.validator.ts | 18 ++++ .../validators/match-password.validator.ts | 20 ++++ src/scss/constants.scss | 1 + src/scss/ui/button.scss | 23 +++++ src/scss/ui/input.scss | 21 ++++ 47 files changed, 928 insertions(+), 15 deletions(-) create mode 100644 src/assets/config/dev.config.json create mode 100644 src/iap/modules/auth/auth.module.ts create mode 100644 src/iap/modules/auth/components/login-form/login-form.component.html create mode 100644 src/iap/modules/auth/components/login-form/login-form.component.scss create mode 100644 src/iap/modules/auth/components/login-form/login-form.component.ts create mode 100644 src/iap/modules/auth/components/login-page/login-page.component.html create mode 100644 src/iap/modules/auth/components/login-page/login-page.component.scss create mode 100644 src/iap/modules/auth/components/login-page/login-page.component.ts create mode 100644 src/iap/modules/auth/components/signup-form/signup-form.component.html create mode 100644 src/iap/modules/auth/components/signup-form/signup-form.component.scss create mode 100644 src/iap/modules/auth/components/signup-form/signup-form.component.ts create mode 100644 src/iap/modules/auth/components/signup-page/signup-page.component.html create mode 100644 src/iap/modules/auth/components/signup-page/signup-page.component.scss create mode 100644 src/iap/modules/auth/components/signup-page/signup-page.component.ts create mode 100644 src/iap/modules/auth/interceptors/http-token.interceptor.ts create mode 100644 src/iap/modules/auth/services/jwt-auth.service.ts create mode 100644 src/iap/shared/constants/auth.ts create mode 100644 src/iap/shared/constants/tokens.ts create mode 100644 src/iap/shared/interfaces/auth-service.interface.ts create mode 100644 src/iap/shared/interfaces/user-service.interface.ts create mode 100644 src/iap/shared/models/auth.models.ts create mode 100644 src/iap/shared/models/dto/jwt-session.dto.ts create mode 100644 src/iap/shared/models/dto/update-profile.dto.ts rename src/iap/shared/models/{dto/game.model.ts => entities/game.entity.ts} (100%) create mode 100644 src/iap/shared/models/entities/profile.entity.ts create mode 100644 src/iap/shared/models/entities/user.entity.ts create mode 100644 src/iap/shared/modules/api/api.module.ts create mode 100644 src/iap/shared/modules/api/services/api.service.ts create mode 100644 src/iap/shared/modules/api/services/user.service.ts create mode 100644 src/iap/shared/services/config.service.ts create mode 100644 src/iap/shared/validators/is-login-unique.validator.ts create mode 100644 src/iap/shared/validators/match-password.validator.ts create mode 100644 src/scss/ui/button.scss create mode 100644 src/scss/ui/input.scss diff --git a/package-lock.json b/package-lock.json index 065c3b6..9763093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@angular/platform-browser": "^14.2.5", "@angular/platform-browser-dynamic": "^14.2.5", "@angular/router": "^14.2.5", + "dayjs": "^1.11.6", "rxjs": "~7.5.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" @@ -5579,6 +5580,11 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz", + "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -18071,6 +18077,11 @@ "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", "dev": true }, + "dayjs": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz", + "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index 0fcb667..0c098d4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@angular/platform-browser": "^14.2.5", "@angular/platform-browser-dynamic": "^14.2.5", "@angular/router": "^14.2.5", + "dayjs": "^1.11.6", "rxjs": "~7.5.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" diff --git a/src/assets/config/dev.config.json b/src/assets/config/dev.config.json new file mode 100644 index 0000000..fea7b65 --- /dev/null +++ b/src/assets/config/dev.config.json @@ -0,0 +1,3 @@ +{ + "api": "http://localhost:3000/" +} diff --git a/src/iap/layout/layout-routing.module.ts b/src/iap/layout/layout-routing.module.ts index f3f542c..9d1c34d 100644 --- a/src/iap/layout/layout-routing.module.ts +++ b/src/iap/layout/layout-routing.module.ts @@ -1,11 +1,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { devPages, shopPages } from '@shared/constants/pages'; +import { authPages, devPages, shopPages } from '@shared/constants/pages'; import { LayoutComponent } from './components/layout/layout.component'; import { HomePageComponent } from '../modules/shop/home/components/home-page/home-page.component'; import { PointsShopPageComponent } from '../modules/shop/points-shop/components/pointsshop-page/pointsshop-page.component'; import { DevPageComponent } from '../modules/dev/components/dev-page/dev-page.component'; +import { SignupPageComponent } from '../modules/auth/components/signup-page/signup-page.component'; +import { LoginPageComponent } from '../modules/auth/components/login-page/login-page.component'; const routes: Routes = [ { @@ -20,6 +22,14 @@ const routes: Routes = [ path: shopPages.pointsshop.name, component: PointsShopPageComponent, }, + { + path: authPages.signup.name, + component: SignupPageComponent, + }, + { + path: authPages.login.name, + component: LoginPageComponent, + }, { path: devPages.dev.name, component: DevPageComponent, diff --git a/src/iap/modules/auth/auth.module.ts b/src/iap/modules/auth/auth.module.ts new file mode 100644 index 0000000..648ef13 --- /dev/null +++ b/src/iap/modules/auth/auth.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; + +import { SharedModule } from '@shared/shared.module'; +import { AUTH_SERVICE } from '@shared/constants/tokens'; +import { JwtAuthService } from './services/jwt-auth.service'; +import { HttpTokenInterceptor } from './interceptors/http-token.interceptor'; +import { SignupFormComponent } from './components/signup-form/signup-form.component'; +import { SignupPageComponent } from './components/signup-page/signup-page.component'; +import { LoginFormComponent } from './components/login-form/login-form.component'; +import { LoginPageComponent } from './components/login-page/login-page.component'; + +// TODO: Add other strategies (and ngxs) +@NgModule({ + declarations: [SignupFormComponent, LoginFormComponent, SignupPageComponent, LoginPageComponent], + imports: [ReactiveFormsModule, SharedModule], + providers: [ + { + provide: AUTH_SERVICE, + useClass: JwtAuthService, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: HttpTokenInterceptor, + multi: true, + }, + ], + exports: [SignupFormComponent, SignupPageComponent, LoginPageComponent], +}) +export class AuthModule {} diff --git a/src/iap/modules/auth/components/login-form/login-form.component.html b/src/iap/modules/auth/components/login-form/login-form.component.html new file mode 100644 index 0000000..54a808a --- /dev/null +++ b/src/iap/modules/auth/components/login-form/login-form.component.html @@ -0,0 +1,41 @@ +
+ +
diff --git a/src/iap/modules/auth/components/login-form/login-form.component.scss b/src/iap/modules/auth/components/login-form/login-form.component.scss new file mode 100644 index 0000000..8e4c237 --- /dev/null +++ b/src/iap/modules/auth/components/login-form/login-form.component.scss @@ -0,0 +1,54 @@ +@import '/src/scss/ui/input.scss'; +@import '/src/scss/ui/button.scss'; +@import '/src/scss/constants'; + +.iap-login { + display: flex; + width: 100%; + + &__form { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; + width: 100%; + } + + &__remember { + width: 100%; + } + + &__submit-button { + @extend %base-button; + width: 180px; + } + + &__error { + height: 20px; + width: 100%; + text-align: center; + } + + &__help { + text-decoration: underline; + + &:hover { + cursor: pointer; + } + } +} + +.iap-input-field { + @extend %base-input-field; + width: 100%; + + &__label { + @extend %base-input-field__label; + text-transform: uppercase; + } + + &__input { + @extend %base-input-field__input; + font-size: calc($text-size * 4 / 3); + } +} diff --git a/src/iap/modules/auth/components/login-form/login-form.component.ts b/src/iap/modules/auth/components/login-form/login-form.component.ts new file mode 100644 index 0000000..24b4cd3 --- /dev/null +++ b/src/iap/modules/auth/components/login-form/login-form.component.ts @@ -0,0 +1,30 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +import { LoginForm } from '@shared/models/auth.models'; + +@Component({ + selector: 'iap-login-form', + templateUrl: './login-form.component.html', + styleUrls: ['./login-form.component.scss'], +}) +export class LoginFormComponent implements OnInit { + @Output() submitEvent: EventEmitter = new EventEmitter(); + + loginForm!: FormGroup; + + constructor(private fb: FormBuilder) {} + + ngOnInit() { + this.loginForm = this.fb.group({ + login: [''], + password: [''], + }); + } + + onSubmit() { + if (this.loginForm.valid) { + this.submitEvent.emit(this.loginForm.value); + } + } +} diff --git a/src/iap/modules/auth/components/login-page/login-page.component.html b/src/iap/modules/auth/components/login-page/login-page.component.html new file mode 100644 index 0000000..8d13996 --- /dev/null +++ b/src/iap/modules/auth/components/login-page/login-page.component.html @@ -0,0 +1,9 @@ + diff --git a/src/iap/modules/auth/components/login-page/login-page.component.scss b/src/iap/modules/auth/components/login-page/login-page.component.scss new file mode 100644 index 0000000..5d1aa48 --- /dev/null +++ b/src/iap/modules/auth/components/login-page/login-page.component.scss @@ -0,0 +1,32 @@ +@import '/src/scss/constants'; + +.iap-login-page { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + + &__form { + width: max(300px, 45vw); + padding: 10px; + border-radius: 0.5rem; + color: #fff; + background: linear-gradient($base-color, $second-color); + } + + &__header { + width: max(300px, 45vw); + font-size: $header-size * 2; + font-weight: 600; + text-align: start; + } + + &__error { + display: flex; + justify-content: center; + align-items: center; + text-transform: capitalize; + color: red; + height: 100%; + } +} diff --git a/src/iap/modules/auth/components/login-page/login-page.component.ts b/src/iap/modules/auth/components/login-page/login-page.component.ts new file mode 100644 index 0000000..41f3330 --- /dev/null +++ b/src/iap/modules/auth/components/login-page/login-page.component.ts @@ -0,0 +1,38 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { shopPages } from '@shared/constants/pages'; +import { AUTH_SERVICE } from '@shared/constants/tokens'; +import { IAuthService } from '@shared/interfaces/auth-service.interface'; +import { LoginForm } from '@shared/models/auth.models'; + +@Component({ + selector: 'iap-login-page', + templateUrl: './login-page.component.html', + styleUrls: ['./login-page.component.scss'], +}) +export class LoginPageComponent implements OnInit { + errorStatus: string = ''; + + constructor(@Inject(AUTH_SERVICE) private authService: IAuthService, private router: Router) {} + + ngOnInit(): void { + if (this.authService.isLoggedIn()) { + this.router.navigateByUrl(shopPages.home.absolutePath); + } + } + + // TODO: add modal window component to notify user about success + onSubmit(form: LoginForm) { + this.authService.login(form.login, form.password).subscribe({ + next: () => { + this.errorStatus = ''; + this.router.navigateByUrl(shopPages.home.absolutePath); + }, + error: (err) => { + console.log(err); + this.errorStatus = err?.message || 'unknown error!'; + }, + }); + } +} diff --git a/src/iap/modules/auth/components/signup-form/signup-form.component.html b/src/iap/modules/auth/components/signup-form/signup-form.component.html new file mode 100644 index 0000000..59e994d --- /dev/null +++ b/src/iap/modules/auth/components/signup-form/signup-form.component.html @@ -0,0 +1,98 @@ + diff --git a/src/iap/modules/auth/components/signup-form/signup-form.component.scss b/src/iap/modules/auth/components/signup-form/signup-form.component.scss new file mode 100644 index 0000000..9a906f9 --- /dev/null +++ b/src/iap/modules/auth/components/signup-form/signup-form.component.scss @@ -0,0 +1,38 @@ +@import '/src/scss/ui/input.scss'; +@import '/src/scss/ui/button.scss'; +@import '/src/scss/constants'; + +.iap-signup { + &__form { + display: flex; + flex-direction: column; + gap: 20px; + } + + &__submit-button { + @extend %base-button; + } +} + +.iap-input-field { + @extend %base-input-field; + + &__label { + @extend %base-input-field__label; + } + + &__input { + @extend %base-input-field__input; + font-size: calc($text-size * 4 / 3); + resize: none; + } + + &__errors { + color: red; + margin-top: 5px; + + h4 { + margin: 0; + } + } +} diff --git a/src/iap/modules/auth/components/signup-form/signup-form.component.ts b/src/iap/modules/auth/components/signup-form/signup-form.component.ts new file mode 100644 index 0000000..c64c639 --- /dev/null +++ b/src/iap/modules/auth/components/signup-form/signup-form.component.ts @@ -0,0 +1,61 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { SignupForm } from '@shared/models/auth.models'; +import { IsLoginUniqueValidator } from '@shared/validators/is-login-unique.validator'; +import { matchPassword } from '@shared/validators/match-password.validator'; + +@Component({ + selector: 'iap-signup-form', + templateUrl: './signup-form.component.html', + styleUrls: ['./signup-form.component.scss'], +}) +export class SignupFormComponent implements OnInit { + @Output() submitEvent: EventEmitter = new EventEmitter(); + + signupForm!: FormGroup; + + constructor(private fb: FormBuilder, private isLoginUnique: IsLoginUniqueValidator) {} + + ngOnInit() { + this.signupForm = this.fb.group( + { + login: ['', [Validators.required, Validators.email], [this.isLoginUnique]], + username: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(20)]], + birthDate: [''], + description: [''], + password: ['', [Validators.required, Validators.minLength(8), Validators.maxLength(20)]], + passwordConfirmation: ['', [Validators.required]], + }, + { + validators: matchPassword('password', 'passwordConfirmation'), + }, + ); + } + + onSubmit() { + if (this.signupForm.valid) { + this.submitEvent.emit(this.signupForm.value); + } + } + + isControlInvalid(controlName: string) { + const control = this.signupForm.controls?.[controlName]; + + if (!control) { + return null; + } + + return control.invalid && (control.touched || control.dirty); + } + + hasErrors(controlName: string, errorName: string) { + const control = this.signupForm.controls?.[controlName]; + + if (!control) { + return null; + } + + return !!control.errors?.[errorName]; + } +} diff --git a/src/iap/modules/auth/components/signup-page/signup-page.component.html b/src/iap/modules/auth/components/signup-page/signup-page.component.html new file mode 100644 index 0000000..c6e8eb8 --- /dev/null +++ b/src/iap/modules/auth/components/signup-page/signup-page.component.html @@ -0,0 +1,12 @@ + diff --git a/src/iap/modules/auth/components/signup-page/signup-page.component.scss b/src/iap/modules/auth/components/signup-page/signup-page.component.scss new file mode 100644 index 0000000..d6a19c8 --- /dev/null +++ b/src/iap/modules/auth/components/signup-page/signup-page.component.scss @@ -0,0 +1,24 @@ +@import '/src/scss/constants'; + +.iap-signup-page { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 10px 15vw; + + &__error { + width: max(40%, 200px); + color: #fff; + padding: 6px; + background-color: black; + outline: 1px solid red; + font-size: $text-size; + text-transform: capitalize; + } + + &__form { + width: max(40%, 200px); + white-space: nowrap; + } +} diff --git a/src/iap/modules/auth/components/signup-page/signup-page.component.ts b/src/iap/modules/auth/components/signup-page/signup-page.component.ts new file mode 100644 index 0000000..79888b6 --- /dev/null +++ b/src/iap/modules/auth/components/signup-page/signup-page.component.ts @@ -0,0 +1,53 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { tap } from 'rxjs'; + +import { shopPages } from '@shared/constants/pages'; +import { AUTH_SERVICE, USER_SERVICE } from '@shared/constants/tokens'; +import { IAuthService } from '@shared/interfaces/auth-service.interface'; +import { IUserService } from '@shared/interfaces/user-service.interface'; +import { SignupForm } from '@shared/models/auth.models'; + +@Component({ + selector: 'iap-signup-page', + templateUrl: './signup-page.component.html', + styleUrls: ['./signup-page.component.scss'], +}) +export class SignupPageComponent implements OnInit { + errorStatus: string = ''; + + constructor( + @Inject(AUTH_SERVICE) private authService: IAuthService, + @Inject(USER_SERVICE) private userService: IUserService, + private router: Router, + ) {} + + ngOnInit(): void { + if (this.authService.isLoggedIn()) { + this.router.navigateByUrl(shopPages.home.absolutePath); + } + } + + // TODO: add modal window component to notify user about success + onSubmit(form: SignupForm) { + this.authService + .signup(form.login, form.password, form.username, form.passwordConfirmation) + .pipe( + tap(() => { + return this.authService.login(form.login, form.password).subscribe(() => { + return this.userService.updateProfile({ ...form }).subscribe(); + }); + }), + ) + .subscribe({ + next: () => { + window.alert('You have been successfully registered!'); + this.errorStatus = ''; + this.router.navigateByUrl(shopPages.home.absolutePath); + }, + error: (err) => { + this.errorStatus = err?.message || 'unknown error!'; + }, + }); + } +} diff --git a/src/iap/modules/auth/interceptors/http-token.interceptor.ts b/src/iap/modules/auth/interceptors/http-token.interceptor.ts new file mode 100644 index 0000000..7f8d097 --- /dev/null +++ b/src/iap/modules/auth/interceptors/http-token.interceptor.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ID_TOKEN } from '@shared/constants/auth'; + +@Injectable() +export class HttpTokenInterceptor implements HttpInterceptor { + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const headers: any = { + Accept: 'application/json', + }; + + const token = localStorage.getItem(ID_TOKEN); + console.log(token || 'нету токена'); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const request = req.clone({ setHeaders: headers }); + return next.handle(request); + } +} diff --git a/src/iap/modules/auth/services/jwt-auth.service.ts b/src/iap/modules/auth/services/jwt-auth.service.ts new file mode 100644 index 0000000..069e478 --- /dev/null +++ b/src/iap/modules/auth/services/jwt-auth.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { map, Observable, shareReplay, tap } from 'rxjs'; +import * as dayjs from 'dayjs'; + +import { EXPIRES_AT, ID_TOKEN } from '@shared/constants/auth'; +import { JwtSession } from '@shared/models/dto/jwt-session.dto'; +import { User } from '@shared/models/entities/user.entity'; +import { IAuthService } from '@shared/interfaces/auth-service.interface'; +import { ApiService } from '@shared/modules/api/services/api.service'; + +// TODO: add refresh token to this strategy +@Injectable({ providedIn: 'root' }) +export class JwtAuthService implements IAuthService { + constructor(private api: ApiService) {} + + login(login: string, password: string): Observable { + const url = 'auth/login'; + return this.api.post(url, { login, password }).pipe( + tap((res: any) => this.setSession(res as JwtSession)), + shareReplay(), + ); + } + + loginAdmin(login: string, password: string): Observable { + const url = 'auth/login-admin'; + return this.api.post(url, { login, password }).pipe( + tap((res: any) => this.setSession(res as JwtSession)), + shareReplay(), + ); + } + + signup(login: string, password: string, username: string, passwordConfirmation: string): Observable { + const url = 'auth/signup'; + return this.api + .post(url, { login, password, passwordConfirmation, username }) + .pipe(map((user: any) => new User(user.id, user.login))); + } + + logout() { + localStorage.removeItem(ID_TOKEN); + localStorage.removeItem(EXPIRES_AT); + } + + isLoggedIn() { + return dayjs().isBefore(this.getExpiration()); + } + + getExpiration() { + const expiration = localStorage.getItem(EXPIRES_AT); + const expiresAt = JSON.parse(expiration!); + return dayjs(expiresAt); + } + + private setSession(jwt: JwtSession) { + const expiresAt = dayjs().add(Number.parseInt(jwt.expiresIn, 10), 'hours'); + localStorage.setItem(ID_TOKEN, jwt.accessToken); + localStorage.setItem(EXPIRES_AT, JSON.stringify(expiresAt.valueOf())); + } +} diff --git a/src/iap/modules/dev/components/dev-page/dev-page.component.html b/src/iap/modules/dev/components/dev-page/dev-page.component.html index 20be642..a38d38d 100644 --- a/src/iap/modules/dev/components/dev-page/dev-page.component.html +++ b/src/iap/modules/dev/components/dev-page/dev-page.component.html @@ -7,4 +7,8 @@

DEV

+ + diff --git a/src/iap/modules/dev/components/dev-page/dev-page.component.scss b/src/iap/modules/dev/components/dev-page/dev-page.component.scss index d567cc7..56cdd7a 100644 --- a/src/iap/modules/dev/components/dev-page/dev-page.component.scss +++ b/src/iap/modules/dev/components/dev-page/dev-page.component.scss @@ -13,8 +13,6 @@ height: 400px; } -.asymetric-board-container { - width: 900px; - height: 400px; - padding: 50px; +.signup-form-container { + width: 400px; } diff --git a/src/iap/modules/dev/components/dev-page/dev-page.component.ts b/src/iap/modules/dev/components/dev-page/dev-page.component.ts index a16fac7..738ff8b 100644 --- a/src/iap/modules/dev/components/dev-page/dev-page.component.ts +++ b/src/iap/modules/dev/components/dev-page/dev-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { PreviewSlideComponent } from '@shared/components'; import { Slide } from '@shared/models/carousel.models'; -import { Game } from '@shared/models/dto/game.model'; +import { Game } from '@shared/models/entities/game.entity'; @Component({ selector: 'iap-dev-page', diff --git a/src/iap/modules/dev/dev.module.ts b/src/iap/modules/dev/dev.module.ts index b08d3b1..3523c80 100644 --- a/src/iap/modules/dev/dev.module.ts +++ b/src/iap/modules/dev/dev.module.ts @@ -1,11 +1,12 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '@shared/shared.module'; +import { AuthModule } from '../auth/auth.module'; import { DevPageComponent } from './components/dev-page/dev-page.component'; @NgModule({ declarations: [DevPageComponent], - imports: [SharedModule], + imports: [SharedModule, AuthModule], exports: [DevPageComponent], }) export class DevModule {} diff --git a/src/iap/modules/modules.module.ts b/src/iap/modules/modules.module.ts index a5ed470..dc6e308 100644 --- a/src/iap/modules/modules.module.ts +++ b/src/iap/modules/modules.module.ts @@ -1,9 +1,10 @@ import { NgModule } from '@angular/core'; +import { AuthModule } from './auth/auth.module'; import { DevModule } from './dev/dev.module'; import { ShopModule } from './shop/shop.module'; @NgModule({ - exports: [ShopModule, DevModule], + exports: [ShopModule, DevModule, AuthModule], }) export class ModulesModule {} diff --git a/src/iap/shared/components/game-card/game-card.component.ts b/src/iap/shared/components/game-card/game-card.component.ts index 226d864..4976b61 100644 --- a/src/iap/shared/components/game-card/game-card.component.ts +++ b/src/iap/shared/components/game-card/game-card.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; -import { Game } from '@shared/models/dto/game.model'; +import { Game } from '@shared/models/entities/game.entity'; @Component({ selector: 'iap-game-card', diff --git a/src/iap/shared/components/preview-slide/preview-slide.component.ts b/src/iap/shared/components/preview-slide/preview-slide.component.ts index 088d4b7..06710fb 100644 --- a/src/iap/shared/components/preview-slide/preview-slide.component.ts +++ b/src/iap/shared/components/preview-slide/preview-slide.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core'; import { SlideComponent } from '@shared/models/carousel.models'; -import { Game } from '@shared/models/dto/game.model'; +import { Game } from '@shared/models/entities/game.entity'; @Component({ selector: 'iap-preview-slide', diff --git a/src/iap/shared/constants/auth.ts b/src/iap/shared/constants/auth.ts new file mode 100644 index 0000000..f97994d --- /dev/null +++ b/src/iap/shared/constants/auth.ts @@ -0,0 +1,2 @@ +export const ID_TOKEN = 'iap_id_token'; +export const EXPIRES_AT = 'iap_expires_at'; diff --git a/src/iap/shared/constants/pages.ts b/src/iap/shared/constants/pages.ts index cd040ce..cac323e 100644 --- a/src/iap/shared/constants/pages.ts +++ b/src/iap/shared/constants/pages.ts @@ -20,6 +20,12 @@ export const devPages = { dev: new Page('DEV', 'dev', '/dev'), }; +export const authPages = { + signup: new Page('Sign Up', 'signup', '/signup'), + login: new Page('Login', 'login', '/login'), +}; + export const shopPagesArray = Object.values(shopPages); export const communityPagesArray = Object.values(communityPages); export const devPagesArray = Object.values(devPages); +export const autPagesArray = Object.values(authPages); diff --git a/src/iap/shared/constants/tokens.ts b/src/iap/shared/constants/tokens.ts new file mode 100644 index 0000000..a029fd3 --- /dev/null +++ b/src/iap/shared/constants/tokens.ts @@ -0,0 +1,2 @@ +export const AUTH_SERVICE = 'IAP_AUTH_SERVICE'; +export const USER_SERVICE = 'IAP_USER_SERVICE'; diff --git a/src/iap/shared/interfaces/auth-service.interface.ts b/src/iap/shared/interfaces/auth-service.interface.ts new file mode 100644 index 0000000..3b6403a --- /dev/null +++ b/src/iap/shared/interfaces/auth-service.interface.ts @@ -0,0 +1,11 @@ +import { Observable } from 'rxjs'; + +import { User } from '@shared/models/entities/user.entity'; + +export interface IAuthService { + login(login: string, password: string): Observable; + loginAdmin(login: string, password: string): Observable; + signup(login: string, password: string, username: string, passwordConfirmation: string): Observable; + isLoggedIn(): boolean; + logout(): void; +} diff --git a/src/iap/shared/interfaces/user-service.interface.ts b/src/iap/shared/interfaces/user-service.interface.ts new file mode 100644 index 0000000..926da4d --- /dev/null +++ b/src/iap/shared/interfaces/user-service.interface.ts @@ -0,0 +1,10 @@ +import { Observable } from 'rxjs'; + +import { User } from '@shared/models/entities/user.entity'; +import { Profile } from '@shared/models/entities/profile.entity'; +import { UpdateProfileDto } from '@shared/models/dto/update-profile.dto'; + +export interface IUserService { + getByLogin(login: string): Observable; + updateProfile(profile: UpdateProfileDto): Observable; +} diff --git a/src/iap/shared/models/auth.models.ts b/src/iap/shared/models/auth.models.ts new file mode 100644 index 0000000..6825973 --- /dev/null +++ b/src/iap/shared/models/auth.models.ts @@ -0,0 +1,13 @@ +export interface SignupForm { + login: string; + username: string; + birthDate?: string; + description?: string; + password: string; + passwordConfirmation: string; +} + +export interface LoginForm { + login: string; + password: string; +} diff --git a/src/iap/shared/models/dto/jwt-session.dto.ts b/src/iap/shared/models/dto/jwt-session.dto.ts new file mode 100644 index 0000000..f69a353 --- /dev/null +++ b/src/iap/shared/models/dto/jwt-session.dto.ts @@ -0,0 +1,4 @@ +export interface JwtSession { + accessToken: string; + expiresIn: string; +} diff --git a/src/iap/shared/models/dto/update-profile.dto.ts b/src/iap/shared/models/dto/update-profile.dto.ts new file mode 100644 index 0000000..68b07ac --- /dev/null +++ b/src/iap/shared/models/dto/update-profile.dto.ts @@ -0,0 +1,5 @@ +export interface UpdateProfileDto { + username?: string; + description?: string; + birthDate?: string; +} diff --git a/src/iap/shared/models/dto/game.model.ts b/src/iap/shared/models/entities/game.entity.ts similarity index 100% rename from src/iap/shared/models/dto/game.model.ts rename to src/iap/shared/models/entities/game.entity.ts diff --git a/src/iap/shared/models/entities/profile.entity.ts b/src/iap/shared/models/entities/profile.entity.ts new file mode 100644 index 0000000..f1d16f5 --- /dev/null +++ b/src/iap/shared/models/entities/profile.entity.ts @@ -0,0 +1,10 @@ +// TODO: extend profile entity +export class Profile { + constructor( + public id: number, + public userId: number, + public username: string, + public description: string, + public birthDate: string, + ) {} +} diff --git a/src/iap/shared/models/entities/user.entity.ts b/src/iap/shared/models/entities/user.entity.ts new file mode 100644 index 0000000..fbfb397 --- /dev/null +++ b/src/iap/shared/models/entities/user.entity.ts @@ -0,0 +1,3 @@ +export class User { + constructor(public id: number, public login: string) {} +} diff --git a/src/iap/shared/modules/api/api.module.ts b/src/iap/shared/modules/api/api.module.ts new file mode 100644 index 0000000..4307403 --- /dev/null +++ b/src/iap/shared/modules/api/api.module.ts @@ -0,0 +1,18 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { USER_SERVICE } from '@shared/constants/tokens'; + +import { ApiService } from './services/api.service'; +import { UserService } from './services/user.service'; + +@NgModule({ + imports: [HttpClientModule], + providers: [ + ApiService, + { + provide: USER_SERVICE, + useClass: UserService, + }, + ], +}) +export class ApiModule {} diff --git a/src/iap/shared/modules/api/services/api.service.ts b/src/iap/shared/modules/api/services/api.service.ts new file mode 100644 index 0000000..3780777 --- /dev/null +++ b/src/iap/shared/modules/api/services/api.service.ts @@ -0,0 +1,44 @@ +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { catchError, throwError } from 'rxjs'; + +import { ConfigService } from '@shared/services/config.service'; + +@Injectable() +export class ApiService { + api!: string; + constructor(private http: HttpClient, private config: ConfigService) { + this.api = this.config.settings.api; + } + + get(path: string) { + return this.http.get(`${this.api}${path}`).pipe(catchError(this.handleError)); + } + + delete(path: string) { + return this.http.delete(`${this.api}${path}`).pipe(catchError(this.handleError)); + } + + post(path: string, body: Object = {}) { + return this.http.post(`${this.api}${path}`, body).pipe(catchError(this.handleError)); + } + + put(path: string, body: Object = {}) { + return this.http.put(`${this.api}${path}`, body).pipe(catchError(this.handleError)); + } + + patch(path: string, body: Object = {}) { + return this.http.patch(`${this.api}${path}`, body).pipe(catchError(this.handleError)); + } + + uploadFile(path: string, formData: FormData) { + const headers = new HttpHeaders(); + headers.set('Content-Type', 'multipart/form-data'); + return this.http.post(`${this.api}${path}`, formData, { headers }).pipe(catchError(this.handleError)); + } + + private handleError(error: HttpErrorResponse) { + // TODO: Add logging of errors + return throwError(() => error.error); + } +} diff --git a/src/iap/shared/modules/api/services/user.service.ts b/src/iap/shared/modules/api/services/user.service.ts new file mode 100644 index 0000000..746a5a3 --- /dev/null +++ b/src/iap/shared/modules/api/services/user.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { Observable, map } from 'rxjs'; + +import { User } from '@shared/models/entities/user.entity'; +import { Profile } from '@shared/models/entities/profile.entity'; +import { UpdateProfileDto } from '@shared/models/dto/update-profile.dto'; +import { IUserService } from '@shared/interfaces/user-service.interface'; +import { ApiService } from './api.service'; + +@Injectable() +export class UserService implements IUserService { + constructor(private api: ApiService) {} + + get(id: number): Observable { + const url = `user/${id}`; + return this.api.get(url).pipe(map((user: any) => new User(user?.id, user?.login))); + } + + getByLogin(login: string): Observable { + const url = `user/get-by-login`; + return this.api.post(url, { login }).pipe(map((user: any) => new User(user?.id, user?.login))); + } + + updateProfile(profile: UpdateProfileDto): Observable { + const url = `userinfo/update-profile`; + return this.api.put(url, profile).pipe( + map((profile: any) => { + const { id, userId, username, description, birthDate } = profile; + return new Profile(id, userId, username, description, birthDate); + }), + ); + } +} diff --git a/src/iap/shared/services/config.service.ts b/src/iap/shared/services/config.service.ts new file mode 100644 index 0000000..04eb195 --- /dev/null +++ b/src/iap/shared/services/config.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class ConfigService { + settings: any; + constructor(private http: HttpClient) {} + + async load() { + const jsonFile = `assets/config/dev.config.json`; + return new Promise((resolve, reject) => { + firstValueFrom(this.http.get(jsonFile)) + .then((response: any) => { + this.settings = response; + resolve(); + }) + .catch((response: any) => { + reject(new Error(`Cannot get configuration: ${response}`)); + }); + }); + } +} diff --git a/src/iap/shared/shared.module.ts b/src/iap/shared/shared.module.ts index e276b28..0d18893 100644 --- a/src/iap/shared/shared.module.ts +++ b/src/iap/shared/shared.module.ts @@ -1,13 +1,27 @@ -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; +import { ApiModule } from './modules/api/api.module'; +import { ConfigService } from './services/config.service'; import { DropdownComponent, CarouselComponent, GameCardComponent, PreviewSlideComponent } from './components'; -import { SlideDirective } from './directives/slide.directive'; +import { IsLoginUniqueValidator } from './validators/is-login-unique.validator'; @NgModule({ - declarations: [DropdownComponent, CarouselComponent, GameCardComponent, PreviewSlideComponent, SlideDirective], - imports: [CommonModule, RouterModule], - exports: [DropdownComponent, CarouselComponent, GameCardComponent, PreviewSlideComponent, SlideDirective], + declarations: [DropdownComponent, CarouselComponent, GameCardComponent, PreviewSlideComponent], + imports: [CommonModule, RouterModule, ApiModule], + providers: [ + ConfigService, + { + provide: APP_INITIALIZER, + useFactory: (config: ConfigService) => { + return () => config.load(); + }, + deps: [ConfigService], + multi: true, + }, + IsLoginUniqueValidator, + ], + exports: [CommonModule, DropdownComponent, CarouselComponent, GameCardComponent, PreviewSlideComponent], }) export class SharedModule {} diff --git a/src/iap/shared/validators/is-login-unique.validator.ts b/src/iap/shared/validators/is-login-unique.validator.ts new file mode 100644 index 0000000..e7ecea4 --- /dev/null +++ b/src/iap/shared/validators/is-login-unique.validator.ts @@ -0,0 +1,18 @@ +import { Injectable, Inject } from '@angular/core'; +import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms'; +import { catchError, map, Observable, of } from 'rxjs'; + +import { USER_SERVICE } from '@shared/constants/tokens'; +import { IUserService } from '@shared/interfaces/user-service.interface'; + +@Injectable() +export class IsLoginUniqueValidator implements AsyncValidator { + constructor(@Inject(USER_SERVICE) private userService: IUserService) {} + + validate(control: AbstractControl): Observable { + return this.userService.getByLogin(control.value).pipe( + map((user) => (user.id ? { hasTaken: true } : null)), + catchError(() => of(null)), + ); + } +} diff --git a/src/iap/shared/validators/match-password.validator.ts b/src/iap/shared/validators/match-password.validator.ts new file mode 100644 index 0000000..fc009d1 --- /dev/null +++ b/src/iap/shared/validators/match-password.validator.ts @@ -0,0 +1,20 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export function matchPassword(password: string, passwordConfirmation: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const passwordControl = control.get(password); + const passwordConfirmationControl = control.get(passwordConfirmation); + + if (!passwordConfirmationControl || !passwordControl) { + return null; + } + + if (passwordControl.value !== passwordConfirmationControl.value) { + passwordConfirmationControl.setErrors({ passwordMismatch: true }); + return { passwordMismatch: true }; + } + + passwordConfirmationControl.setErrors(null); + return null; + }; +} diff --git a/src/scss/constants.scss b/src/scss/constants.scss index dc2d9b9..463f666 100644 --- a/src/scss/constants.scss +++ b/src/scss/constants.scss @@ -7,6 +7,7 @@ $base-color: #110c5c; $second-color: #5c0168; $nav-link-color: #d6d6d6; $nav-link-active-color: #fff; +$input-background-color: #19123a; @font-face { font-family: Vinslab; diff --git a/src/scss/ui/button.scss b/src/scss/ui/button.scss new file mode 100644 index 0000000..a3e131b --- /dev/null +++ b/src/scss/ui/button.scss @@ -0,0 +1,23 @@ +@import '../constants.scss'; + +//TODO: use regular classes maybe +%base-button { + font-size: $header-size; + padding: 8px 12px; + background-color: #377aff; + color: #fff; + border-radius: 0.2em; + border: 0.2em solid #377aff; + transition: all 0.2s ease; + + &:hover:enabled { + cursor: pointer; + color: #000; + background-color: #fff; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} diff --git a/src/scss/ui/input.scss b/src/scss/ui/input.scss new file mode 100644 index 0000000..29ac8d3 --- /dev/null +++ b/src/scss/ui/input.scss @@ -0,0 +1,21 @@ +@import '../constants.scss'; + +//TODO: use regular classes maybe +%base-input-field { + display: flex; + flex-direction: column; +} + +%base-input-field__label { +} + +%base-input-field__input { + padding: 8px 6px; + background-color: $input-background-color; + color: #fff; + border-radius: 0.2rem; + + &:focus { + outline: 1px solid #fff; + } +} From 2facdd04e475543b7ef322a79f065f78990de0cc Mon Sep 17 00:00:00 2001 From: Turin Andrey Date: Thu, 3 Nov 2022 20:03:02 +0300 Subject: [PATCH 2/3] refactoring, added environment service to replace config service --- .gitignore | 3 + angular.json | 214 +++++++++--------- src/environments/environment.prod.ts | 1 + src/environments/environment.ts | 14 +- src/iap/iap.module.ts | 3 + .../components/layout/layout.component.html | 2 +- src/iap/layout/layout-routing.module.ts | 18 +- src/iap/modules/auth/auth.module.ts | 17 +- .../login-form/login-form.component.html | 12 +- .../login-form/login-form.component.scss | 2 +- .../login-form/login-form.component.ts | 27 ++- .../login-page/login-page.component.ts | 26 +-- .../signup-form/signup-form.component.html | 53 +++-- .../signup-form/signup-form.component.scss | 2 +- .../signup-form/signup-form.component.ts | 47 ++-- .../signup-page/signup-page.component.ts | 43 +--- .../auth/services/auth-forms-api.service.ts | 22 ++ src/iap/shared/constants/tokens.ts | 5 +- src/iap/shared/directives/slide.directive.ts | 8 - .../interceptors/http-error.interceptor.ts | 31 +++ .../interceptors/http-token.interceptor.ts | 6 +- .../interfaces/auth-service.interface.ts | 11 - .../interfaces/user-service.interface.ts | 10 - src/iap/shared/models/auth.models.ts | 6 + src/iap/shared/models/types/iap-error.type.ts | 3 + src/iap/shared/modules/api/api.module.ts | 10 +- .../modules/api/services/api.service.ts | 44 ---- .../modules/api/services/user.service.ts | 5 +- .../pipes/is-control-has-errors.pipe.ts | 18 ++ .../shared/pipes/is-control-invalid.pipe.ts | 18 ++ .../shared/providers/environment.provider.ts | 11 + .../shared/providers/interceptors.provider.ts | 18 ++ src/iap/shared/services/api.service.ts | 38 ++++ src/iap/shared/services/config.service.ts | 23 -- .../shared/services/environment.service.ts | 11 + .../services/jwt-auth.service.ts | 5 +- src/iap/shared/shared.module.ts | 38 ++-- .../iap-sync-validators.validator.ts | 22 ++ .../validators/is-login-unique.validator.ts | 7 +- 39 files changed, 472 insertions(+), 382 deletions(-) create mode 100644 src/iap/modules/auth/services/auth-forms-api.service.ts delete mode 100644 src/iap/shared/directives/slide.directive.ts create mode 100644 src/iap/shared/interceptors/http-error.interceptor.ts rename src/iap/{modules/auth => shared}/interceptors/http-token.interceptor.ts (84%) delete mode 100644 src/iap/shared/interfaces/auth-service.interface.ts delete mode 100644 src/iap/shared/interfaces/user-service.interface.ts create mode 100644 src/iap/shared/models/types/iap-error.type.ts delete mode 100644 src/iap/shared/modules/api/services/api.service.ts create mode 100644 src/iap/shared/pipes/is-control-has-errors.pipe.ts create mode 100644 src/iap/shared/pipes/is-control-invalid.pipe.ts create mode 100644 src/iap/shared/providers/environment.provider.ts create mode 100644 src/iap/shared/providers/interceptors.provider.ts create mode 100644 src/iap/shared/services/api.service.ts delete mode 100644 src/iap/shared/services/config.service.ts create mode 100644 src/iap/shared/services/environment.service.ts rename src/iap/{modules/auth => shared}/services/jwt-auth.service.ts (89%) create mode 100644 src/iap/shared/validators/iap-sync-validators.validator.ts diff --git a/.gitignore b/.gitignore index 0711527..f592fd3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ testem.log # System files .DS_Store Thumbs.db + +# Local +environment.local.ts diff --git a/angular.json b/angular.json index cdcc9ed..b31bb2c 100644 --- a/angular.json +++ b/angular.json @@ -1,106 +1,112 @@ { - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "itechart-angular": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "style": "scss" - }, - "@schematics/angular:application": { - "strict": true - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "iap", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/itechart-angular", - "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "tsconfig.app.json", - "inlineStyleLanguage": "scss", - "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.scss"], - "scripts": [] - }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" - } - ], - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], - "outputHashing": "all" - }, - "development": { - "buildOptimizer": false, - "optimization": false, - "vendorChunk": true, - "extractLicenses": false, - "sourceMap": true, - "namedChunks": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "configurations": { - "production": { - "browserTarget": "itechart-angular:build:production" - }, - "development": { - "browserTarget": "itechart-angular:build:development" - } - }, - "defaultConfiguration": "development" - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "itechart-angular:build" - } - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "tsconfig.spec.json", - "karmaConfig": "karma.conf.js", - "inlineStyleLanguage": "scss", - "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.scss"], - "scripts": [] - } - }, - "lint": { - "builder": "@angular-eslint/builder:lint", - "options": { - "lintFilePatterns": ["src/**/*.ts", "src/**/*.component.html"] - } - } - } - } - } + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "itechart-angular": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "iap", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/itechart-angular", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.local.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "itechart-angular:build:production" + }, + "development": { + "browserTarget": "itechart-angular:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "itechart-angular:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": ["src/**/*.ts", "src/**/*.component.html"] + } + } + } + } + } } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index d65fc9d..260e82f 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { production: true, + apiUrl: 'http://localhost:3000/', }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index d531fcb..4c10516 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,16 +1,4 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - export const environment = { production: false, + apiUrl: 'http://localhost:3000/', }; - -/* - * For easier debugging in development mode, you can import the following file - * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. - * - * This import should be commented out in production mode because it will have a negative impact - * on performance if an error is thrown. - */ -// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/src/iap/iap.module.ts b/src/iap/iap.module.ts index 4e91e14..c2b2bf6 100644 --- a/src/iap/iap.module.ts +++ b/src/iap/iap.module.ts @@ -1,6 +1,8 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; +import { interceptorsProvider } from '@shared/providers/interceptors.provider'; +import { environmentProviders } from '@shared/providers/environment.provider'; import { IapRoutingModule } from './iap-routing.module'; import { LayoutModule } from './layout/layout.module'; import { IapComponent } from './iap.component'; @@ -8,6 +10,7 @@ import { IapComponent } from './iap.component'; @NgModule({ declarations: [IapComponent], imports: [BrowserModule, IapRoutingModule, LayoutModule], + providers: [...interceptorsProvider, ...environmentProviders], bootstrap: [IapComponent], }) export class IapModule {} diff --git a/src/iap/layout/components/layout/layout.component.html b/src/iap/layout/components/layout/layout.component.html index 59d2403..d3f2275 100644 --- a/src/iap/layout/components/layout/layout.component.html +++ b/src/iap/layout/components/layout/layout.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/iap/layout/layout-routing.module.ts b/src/iap/layout/layout-routing.module.ts index 9d1c34d..5f395bb 100644 --- a/src/iap/layout/layout-routing.module.ts +++ b/src/iap/layout/layout-routing.module.ts @@ -1,4 +1,4 @@ -import { NgModule } from '@angular/core'; +import { isDevMode, NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { authPages, devPages, shopPages } from '@shared/constants/pages'; @@ -9,6 +9,17 @@ import { DevPageComponent } from '../modules/dev/components/dev-page/dev-page.co import { SignupPageComponent } from '../modules/auth/components/signup-page/signup-page.component'; import { LoginPageComponent } from '../modules/auth/components/login-page/login-page.component'; +let devRoutes: Routes = []; + +if (isDevMode()) { + devRoutes = [ + { + path: devPages.dev.name, + component: DevPageComponent, + }, + ]; +} + const routes: Routes = [ { path: '', @@ -30,10 +41,7 @@ const routes: Routes = [ path: authPages.login.name, component: LoginPageComponent, }, - { - path: devPages.dev.name, - component: DevPageComponent, - }, + ...devRoutes, ], }, ]; diff --git a/src/iap/modules/auth/auth.module.ts b/src/iap/modules/auth/auth.module.ts index 648ef13..a41dd5a 100644 --- a/src/iap/modules/auth/auth.module.ts +++ b/src/iap/modules/auth/auth.module.ts @@ -1,31 +1,18 @@ import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { SharedModule } from '@shared/shared.module'; -import { AUTH_SERVICE } from '@shared/constants/tokens'; -import { JwtAuthService } from './services/jwt-auth.service'; -import { HttpTokenInterceptor } from './interceptors/http-token.interceptor'; import { SignupFormComponent } from './components/signup-form/signup-form.component'; import { SignupPageComponent } from './components/signup-page/signup-page.component'; import { LoginFormComponent } from './components/login-form/login-form.component'; import { LoginPageComponent } from './components/login-page/login-page.component'; +import { AuthFormsApiService } from './services/auth-forms-api.service'; // TODO: Add other strategies (and ngxs) @NgModule({ declarations: [SignupFormComponent, LoginFormComponent, SignupPageComponent, LoginPageComponent], imports: [ReactiveFormsModule, SharedModule], - providers: [ - { - provide: AUTH_SERVICE, - useClass: JwtAuthService, - }, - { - provide: HTTP_INTERCEPTORS, - useClass: HttpTokenInterceptor, - multi: true, - }, - ], + providers: [AuthFormsApiService], exports: [SignupFormComponent, SignupPageComponent, LoginPageComponent], }) export class AuthModule {} diff --git a/src/iap/modules/auth/components/login-form/login-form.component.html b/src/iap/modules/auth/components/login-form/login-form.component.html index 54a808a..66770fe 100644 --- a/src/iap/modules/auth/components/login-form/login-form.component.html +++ b/src/iap/modules/auth/components/login-form/login-form.component.html @@ -1,6 +1,6 @@ -