Compare commits

..

4 Commits

Author SHA1 Message Date
Florian THIERRY
5e5792f17c Code moving and fix login component style. 2023-12-06 14:09:23 +01:00
Florian THIERRY
9f40a6c782 Implementation of login and logout mechanisms. 2023-12-05 14:31:07 +01:00
Florian THIERRY
c095cdab3a Add login and home components in angular app. 2023-12-05 11:32:59 +01:00
Florian THIERRY
cea35955e4 Add angular app. 2023-12-04 10:36:12 +01:00
49 changed files with 18457 additions and 10 deletions

5
.gitignore vendored
View File

@@ -32,4 +32,7 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
**/docker/postgresql/pgdata **/docker/postgresql/pgdata
**/node_modules
**/.angular

View File

@@ -29,10 +29,6 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.auth0</groupId> <groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId> <artifactId>java-jwt</artifactId>

View File

@@ -1,9 +1,10 @@
package org.sportshub.application.security; package org.sportshub.exposition.configuration.security;
import java.io.IOException; import java.io.IOException;
import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.util.ObjectUtils.isEmpty; import static org.springframework.util.ObjectUtils.isEmpty;
import org.sportshub.application.security.JwtService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;

View File

@@ -1,10 +1,9 @@
package org.sportshub.exposition.configuration; package org.sportshub.exposition.configuration.security;
import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.OPTIONS; import static org.springframework.http.HttpMethod.OPTIONS;
import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpMethod.POST;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
import org.sportshub.application.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
@@ -12,8 +11,6 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

View File

@@ -2,6 +2,8 @@ package org.sportshub.exposition.user;
import java.util.List; import java.util.List;
import org.sportshub.application.security.annotation.AllowedToAdmins;
import org.sportshub.application.security.annotation.AllowedToAnonymous;
import org.sportshub.application.user.UserUseCases; import org.sportshub.application.user.UserUseCases;
import org.sportshub.domain.user.model.User; import org.sportshub.domain.user.model.User;
import org.sportshub.domain.user.model.UserAuthenticationData; import org.sportshub.domain.user.model.UserAuthenticationData;
@@ -24,12 +26,14 @@ public class UserController {
} }
@PostMapping("/login") @PostMapping("/login")
@AllowedToAnonymous
public LoginResponse login(@RequestBody LoginRequest request) { public LoginResponse login(@RequestBody LoginRequest request) {
UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.id(), request.password()); UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.id(), request.password());
return new LoginResponse(userAuthenticationData); return new LoginResponse(userAuthenticationData);
} }
@GetMapping @GetMapping
@AllowedToAdmins
public List<User> findAll() { public List<User> findAll() {
return userUseCases.findAll(); return userUseCases.findAll();
} }

View File

@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

102
sportshub-gui/angular.json Normal file
View File

@@ -0,0 +1,102 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"sportshub-gui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/sportshub-gui",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "sportshub-gui:build:production"
},
"development": {
"buildTarget": "sportshub-gui:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "sportshub-gui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:jest",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
}
}

17658
sportshub-gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
{
"name": "sportshub-gui",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/cdk": "^17.0.2",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/material": "^17.0.2",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"ngx-cookie-service": "^17.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.5",
"@angular/cli": "^17.0.5",
"@angular/compiler-cli": "^17.0.0",
"@types/jasmine": "~5.1.0",
"@types/jest": "^29.5.10",
"jasmine-core": "~5.1.0",
"jest": "^29.7.0",
"jest-preset-angular": "^13.1.4",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.2.2"
},
"jest": {
"preset": "jest-preset-angular",
"setupFilesAfterEnv": [
"<rootDir>/src/setupJest.ts"
]
}
}

View File

@@ -0,0 +1,6 @@
{
"/api": {
"target": "http://localhost:8080",
"secure": false
}
}

View File

@@ -0,0 +1,2 @@
<app-header/>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,8 @@
:host {
display: flex;
flex-direction: column;
app-header {
margin-bottom: 1em;
}
}

View File

@@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import {HttpClientModule} from "@angular/common/http";
import {LoginModule} from "./components/login/login.module";
import {HeaderComponent} from "./header/header.component";
@Component({
selector: 'app-root',
standalone: true,
imports: [
CommonModule,
RouterOutlet,
HeaderComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'sportshub-gui';
}

View File

@@ -0,0 +1,14 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';
import {provideHttpClient} from "@angular/common/http";
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideAnimations()
]
};

View File

@@ -0,0 +1,16 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadChildren: () => import('./components/home/home.module').then(module => module.HomeModule)
},
{
path: 'login',
loadChildren: () => import('./components/login/login.module').then(module => module.LoginModule)
},
{
path: 'logout',
loadChildren: () => import('./components/logout/logout.module').then(module => module.LogoutModule)
}
];

View File

@@ -0,0 +1,17 @@
import {AppService} from "./app.service";
describe('In the service AppService', () => {
let service: AppService;
beforeEach(() => {
service = new AppService();
});
describe('The method "test"', () => {
it('should return "true"', () => {
const result = service.test();
expect(result).toEqual(true);
});
});
});

View File

@@ -0,0 +1,8 @@
import {Injectable} from "@angular/core";
@Injectable()
export class AppService {
test(): boolean {
return true;
}
}

View File

@@ -0,0 +1 @@
<h1>Hello world!</h1>

View File

@@ -0,0 +1,10 @@
import {Component} from "@angular/core";
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent {
}

View File

@@ -0,0 +1,25 @@
import {NgModule} from "@angular/core";
import {HomeComponent} from "./home.component";
import {RouterModule} from "@angular/router";
const routes = [
{
path: '',
component: HomeComponent
}
]
@NgModule({
declarations: [
HomeComponent
],
imports: [
RouterModule.forChild(routes),
],
exports: [
HomeComponent
]
})
export class HomeModule {
}

View File

@@ -0,0 +1,13 @@
<form (ngSubmit)="onSubmit()" class="shadowed" [formGroup]="loginForm" ngNativeValidate>
<div>
<label for="id">Identifier</label>
<input id="id" name="id" formControlName="id" class="input" required/>
</div>
<div>
<label for="password">Password</label>
<input id="password" name="password" type="password" formControlName="password" class="input" required/>
</div>
<div>
<button type="submit" class="btn">Validate</button>
</div>
</form>

View File

@@ -0,0 +1,19 @@
:host {
display: flex;
justify-content: center;
form {
display: flex;
flex-direction: column;
border: solid 1px #e8e8e8;
padding: 1em;
border-radius: .5em;
gap: 1em;
div {
display: flex;
flex-direction: column;
justify-content: left;
}
}
}

View File

@@ -0,0 +1,35 @@
import {Component, OnInit} from "@angular/core";
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import {UserRestService} from "../../core/rest-services/user.rest-service";
import {LoginRequest} from "../../core/model/login-request";
import {MatSnackBar} from "@angular/material/snack-bar";
import {LoginService} from "./login.service";
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
loginForm: FormGroup;
isLoginPending: boolean = false;
constructor(
private formBuilder: FormBuilder,
private loginService: LoginService,
private matSnackBar: MatSnackBar,
private userRestService: UserRestService
) {
this.loginForm = this.formBuilder.group({
id: new FormControl(undefined, [Validators.required]),
password: new FormControl(undefined, [Validators.required])
});
}
onSubmit(): void {
if (this.loginForm.valid) {
const loginRequest: LoginRequest = this.loginForm.value;
this.loginService.login(loginRequest);
}
}
}

View File

@@ -0,0 +1,34 @@
import {NgModule} from "@angular/core";
import {LoginComponent} from "./login.component";
import {CoreModule} from "../../core/core.module";
import {MatSnackBarModule} from "@angular/material/snack-bar";
import {RouterModule} from "@angular/router";
import {HttpClientModule} from "@angular/common/http";
import {LoginService} from "./login.service";
const routes = [
{
path: '',
component: LoginComponent
}
]
@NgModule({
declarations: [
LoginComponent
],
providers: [
LoginService
],
imports: [
CoreModule,
RouterModule.forChild(routes),
MatSnackBarModule
],
exports: [
LoginComponent
]
})
export class LoginModule {
}

View File

@@ -0,0 +1,37 @@
import {Injectable} from "@angular/core";
import {UserRestService} from "../../core/rest-services/user.rest-service";
import {LoginRequest} from "../../core/model/login-request";
import {Subject} from "rxjs";
import {MessageService} from "../../core/services/message.service";
import {AuthenticationService} from "../../core/services/authentication.service";
import {Router} from "@angular/router";
@Injectable()
export class LoginService {
private isLoginPending: Subject<boolean> = new Subject<boolean>();
constructor(
private authenticationService: AuthenticationService,
private messageService: MessageService,
private router: Router,
private userRestService: UserRestService
) {}
login(loginRequest: LoginRequest): void {
this.isLoginPending.next(true);
this.userRestService.login(loginRequest)
.then(loginResponse => {
this.messageService.display('Login success!');
this.authenticationService.setAuthenticated(loginResponse);
this.router.navigate(['/']);
})
.catch(error => {
if (error.status === 400) {
this.messageService.display('Login or password incorrect.')
} else {
this.messageService.display('An error occured while login.')
}
});
}
}

View File

@@ -0,0 +1,2 @@
<h1>Disconnection...</h1>
<mat-spinner></mat-spinner>

View File

@@ -0,0 +1,6 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,15 @@
import {Component, inject, OnInit} from "@angular/core";
import {LogoutService} from "./logout.service";
@Component({
selector: 'app-logout',
templateUrl: './logout.component.html',
styleUrls: ['./logout.component.scss']
})
export class LogoutComponent implements OnInit {
private logoutService = inject(LogoutService);
ngOnInit(): void {
this.logoutService.logout();
}
}

View File

@@ -0,0 +1,29 @@
import {NgModule} from "@angular/core";
import {RouterModule} from "@angular/router";
import {LogoutComponent} from "./logout.component";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {LogoutService} from "./logout.service";
const routes = [
{
path: '',
component: LogoutComponent
}
];
@NgModule({
declarations: [
LogoutComponent
],
providers: [
LogoutService
],
imports: [
RouterModule.forChild(routes),
MatProgressSpinnerModule
],
exports: [
LogoutComponent
]
})
export class LogoutModule {}

View File

@@ -0,0 +1,16 @@
import {Injectable} from "@angular/core";
import {AuthenticationService} from "../../core/services/authentication.service";
import {Router} from "@angular/router";
@Injectable()
export class LogoutService {
constructor(
private authenticationService: AuthenticationService,
private router: Router
) {}
logout(): void {
this.authenticationService.setAnonymous();
this.router.navigate(['/']);
}
}

View File

@@ -0,0 +1,17 @@
import {NgModule} from "@angular/core";
import {ReactiveFormsModule} from "@angular/forms";
import {HttpClientModule} from "@angular/common/http";
@NgModule({
imports: [
ReactiveFormsModule,
HttpClientModule,
],
exports: [
ReactiveFormsModule,
HttpClientModule
]
})
export class CoreModule {
}

View File

@@ -0,0 +1,4 @@
export interface LoginRequest {
id: string;
password: string;
}

View File

@@ -0,0 +1,5 @@
export interface LoginResponse {
tokenType: string;
accessToken: string;
refreshToken: string;
}

View File

@@ -0,0 +1,20 @@
import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {LoginResponse} from "../model/login-response";
import {firstValueFrom} from "rxjs";
import {LoginRequest} from "../model/login-request";
@Injectable({
providedIn: 'root'
})
export class UserRestService {
constructor(
private httpClient: HttpClient
) {}
login(request: LoginRequest): Promise<LoginResponse> {
return firstValueFrom(
this.httpClient.post<LoginResponse>('/api/users/login', request)
)
}
}

View File

@@ -0,0 +1,45 @@
import {Injectable} from "@angular/core";
import {CookieService} from "ngx-cookie-service";
import {LoginResponse} from "../model/login-response";
import {BehaviorSubject, Observable} from "rxjs";
const COOKIE_JWT = 'jwt';
const COOKIE_REFRESH_TOKEN = 'refreshToken';
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
private authenticationSubject: BehaviorSubject<boolean>;
constructor(
private cookieService: CookieService
) {
const isAuthenticated = this.isAuthenticated();
this.authenticationSubject = new BehaviorSubject<boolean>(isAuthenticated);
}
get isAuthenticated$(): Observable<boolean> {
return this.authenticationSubject.asObservable();
}
setAuthenticated(loginResponse: LoginResponse): void {
const jwt = loginResponse.accessToken;
this.cookieService.set(COOKIE_JWT, jwt);
const refreshToken = loginResponse.refreshToken;
this.cookieService.set(COOKIE_REFRESH_TOKEN, refreshToken);
this.authenticationSubject.next(true);
}
setAnonymous(): void {
this.cookieService.delete(COOKIE_JWT);
this.cookieService.delete(COOKIE_REFRESH_TOKEN);
this.authenticationSubject.next(false);
}
isAuthenticated(): boolean {
const jwt = this.cookieService.get(COOKIE_JWT);
return jwt?.length > 0;
}
}

View File

@@ -0,0 +1,15 @@
import {inject, Injectable} from "@angular/core";
import {MatSnackBar} from "@angular/material/snack-bar";
const MESSAGE_DURATION = 5000;
@Injectable({
providedIn: 'root'
})
export class MessageService {
private matSnackBar = inject(MatSnackBar);
display(message: string): void {
this.matSnackBar.open(message, 'Close', { duration: MESSAGE_DURATION });
}
}

View File

@@ -0,0 +1,11 @@
<a class="title" routerLink="/">SportsHub</a>
<div id="menu">
<a routerLink="/login" *ngIf="(isAuthenticated$ | async) === false" class="btn">
<mat-icon>login</mat-icon>
Login
</a>
<a routerLink="/logout" *ngIf="isAuthenticated$ | async" class="btn logout">
<mat-icon>logout</mat-icon>
Logout
</a>
</div>

View File

@@ -0,0 +1,34 @@
:host {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: .5em 1em;
background-color: #004680;
.title {
font-size: 2em;
text-decoration: none;
display: flex;
justify-content: center;
align-items: center;
padding: .5em;
color: white;
}
#menu {
display: flex;
flex-direction: row;
gap: 1em;
justify-content: center;
align-items: center;
a {
flex: 0 1;
gap: .5em;
&.logout {
background-color: #c20000;
}
}
}
}

View File

@@ -0,0 +1,28 @@
import {Component} from "@angular/core";
import {RouterLink} from "@angular/router";
import {AuthenticationService} from "../core/services/authentication.service";
import {Observable} from "rxjs";
import {AsyncPipe, NgIf} from "@angular/common";
import {MatIconModule} from "@angular/material/icon";
@Component({
selector: 'app-header',
standalone: true,
templateUrl: './header.component.html',
imports: [
RouterLink,
AsyncPipe,
NgIf,
MatIconModule
],
styleUrls: ['./header.component.scss']
})
export class HeaderComponent {
isAuthenticated$: Observable<boolean>;
constructor(
private authenticationService: AuthenticationService
) {
this.isAuthenticated$ = this.authenticationService.isAuthenticated$;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>SportshubGui</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View File

View File

@@ -0,0 +1,26 @@
/* You can add global styles to this file, and also import other style files */
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
.shadowed {
box-shadow: 0 3px 1px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f;
}
.btn {
background-color: #008cff;
color: white;
border: none;
border-radius: .4em;
display: flex;
justify-content: center;
align-items: center;
padding: .5em 1em;
text-decoration: none;
}
.input {
padding: .5em;
border-radius: .4em;
border: 1px solid #d2d2d2
}

View File

@@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@@ -0,0 +1,32 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}