From e2083fceea1dbae68d8b8d57cfc148a1450e1db1 Mon Sep 17 00:00:00 2001 From: Nikos Anifantis Date: Thu, 13 Feb 2025 14:07:36 +0200 Subject: [PATCH 1/5] refactor(store): isolate ngrx store from app logic via facades --- src/app/app.component.ts | 4 +- src/app/app.config.ts | 16 ++-- src/app/auth/auth.service.ts | 39 +--------- src/app/auth/guards/auth.guard.ts | 4 +- src/app/auth/guards/no-auth.guard.ts | 4 +- src/app/auth/index.ts | 23 +----- src/app/auth/interceptors/auth.interceptor.ts | 78 +++++++++---------- src/app/auth/interceptors/index.ts | 9 +-- src/app/auth/login/login.component.ts | 4 +- src/app/auth/models/auth-facade.model.ts | 14 ++++ src/app/auth/models/auth-user.model.ts | 5 ++ src/app/auth/models/index.ts | 2 + src/app/auth/store/index.ngrx.ts | 27 +++++++ src/app/auth/store/{ => ngrx}/auth.actions.ts | 2 +- src/app/auth/store/{ => ngrx}/auth.effects.ts | 4 +- src/app/auth/store/{ => ngrx}/auth.facade.ts | 6 +- src/app/auth/store/{ => ngrx}/auth.models.ts | 8 +- src/app/auth/store/{ => ngrx}/auth.reducer.ts | 0 .../auth/store/{ => ngrx}/auth.selectors.ts | 0 src/app/auth/store/ngrx/index.ts | 33 ++++++++ src/app/auth/tokens/auth-facade.token.ts | 5 ++ src/app/auth/tokens/index.ts | 1 + .../secured-feat/secured-feat.component.ts | 4 +- 23 files changed, 158 insertions(+), 134 deletions(-) create mode 100644 src/app/auth/models/auth-facade.model.ts create mode 100644 src/app/auth/models/auth-user.model.ts create mode 100644 src/app/auth/models/index.ts create mode 100644 src/app/auth/store/index.ngrx.ts rename src/app/auth/store/{ => ngrx}/auth.actions.ts (95%) rename src/app/auth/store/{ => ngrx}/auth.effects.ts (96%) rename src/app/auth/store/{ => ngrx}/auth.facade.ts (88%) rename src/app/auth/store/{ => ngrx}/auth.models.ts (78%) rename src/app/auth/store/{ => ngrx}/auth.reducer.ts (100%) rename src/app/auth/store/{ => ngrx}/auth.selectors.ts (100%) create mode 100644 src/app/auth/store/ngrx/index.ts create mode 100644 src/app/auth/tokens/auth-facade.token.ts create mode 100644 src/app/auth/tokens/index.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 724f083..2dd52aa 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,7 +4,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { filter } from 'rxjs'; -import { AuthFacade } from './auth'; +import { AUTH_FACADE } from './auth'; import { ConfigService, GoogleAnalyticsService } from './core/services'; import { FooterComponent } from './shared/ui/footer'; import { HeaderComponent } from './shared/ui/header'; @@ -28,7 +28,7 @@ import { HeaderComponent } from './shared/ui/header'; export class AppComponent implements OnInit { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); - private readonly authFacade = inject(AuthFacade); + private readonly authFacade = inject(AUTH_FACADE); private readonly configService = inject(ConfigService); private readonly googleAnalyticsService = inject(GoogleAnalyticsService); diff --git a/src/app/app.config.ts b/src/app/app.config.ts index a5338c2..5f14d2f 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,8 +1,4 @@ -import { - provideHttpClient, - withInterceptors, - withInterceptorsFromDi, -} from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { ApplicationConfig, provideExperimentalZonelessChangeDetection, @@ -17,7 +13,7 @@ import { provideStoreDevtools } from '@ngrx/store-devtools'; import { environment } from '../environments/environment'; import { routes } from './app.routes'; -import { provideAuthStore } from './auth'; +import { provideAuthStore, authInterceptor } from './auth'; import { fakeApiInterceptor } from './core/fake-api'; function provideAppDevTools() { @@ -39,9 +35,11 @@ export const appConfig: ApplicationConfig = { // Setup Interceptors provideHttpClient( - withInterceptorsFromDi(), - // ⚠️ FIXME: remove it in real app ⚠️ - withInterceptors([fakeApiInterceptor]) + withInterceptors([ + authInterceptor, + // ⚠️ FIXME: remove it in real app ⚠️ + fakeApiInterceptor, + ]) ), // Setup Application diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts index 8a95c83..6f9f057 100644 --- a/src/app/auth/auth.service.ts +++ b/src/app/auth/auth.service.ts @@ -1,14 +1,10 @@ import { HttpClient, HttpParams } from '@angular/common/http'; -import { APP_INITIALIZER, Injectable, Provider, inject } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { lastValueFrom, Observable, throwError } from 'rxjs'; -import { filter, take } from 'rxjs/operators'; +import { Injectable, inject } from '@angular/core'; +import { Observable, throwError } from 'rxjs'; import { ConfigService, TokenStorageService } from '../core/services'; -import { RefreshTokenActions } from './store/auth.actions'; -import { AuthState, AuthUser, TokenStatus } from './store/auth.models'; -import * as AuthSelectors from './store/auth.selectors'; +import { AuthUser } from './models'; export interface AccessData { token_type: 'Bearer'; @@ -19,7 +15,6 @@ export interface AccessData { @Injectable({ providedIn: 'root' }) export class AuthService { - private readonly store = inject(Store); private readonly http = inject(HttpClient); private readonly configService = inject(ConfigService); private readonly tokenStorageService = inject(TokenStorageService); @@ -28,27 +23,6 @@ export class AuthService { private readonly clientId = this.configService.getAuthSettings().clientId; private readonly clientSecret = this.configService.getAuthSettings().secretId; - /** - * Returns a promise that waits until - * refresh token and get auth user - * - * @returns {Promise} - */ - init(): Promise { - this.store.dispatch(RefreshTokenActions.request()); - - const authState$ = this.store.select(AuthSelectors.selectAuth).pipe( - filter( - auth => - auth.refreshTokenStatus === TokenStatus.INVALID || - (auth.refreshTokenStatus === TokenStatus.VALID && !!auth.user) - ), - take(1) - ); - - return lastValueFrom(authState$); - } - /** * Performs a request with user credentials * in order to get auth tokens @@ -109,10 +83,3 @@ export class AuthService { return this.http.get(`${this.hostUrl}/api/users/me`); } } - -export const authServiceInitProvider: Provider = { - provide: APP_INITIALIZER, - useFactory: (authService: AuthService) => () => authService.init(), - deps: [AuthService], - multi: true, -}; diff --git a/src/app/auth/guards/auth.guard.ts b/src/app/auth/guards/auth.guard.ts index 1815ba0..71008e9 100644 --- a/src/app/auth/guards/auth.guard.ts +++ b/src/app/auth/guards/auth.guard.ts @@ -6,10 +6,10 @@ import { } from '@angular/router'; import { map, take } from 'rxjs/operators'; -import { AuthFacade } from '../store/auth.facade'; +import { AUTH_FACADE } from '../tokens'; export const authGuard = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - const authFacade = inject(AuthFacade); + const authFacade = inject(AUTH_FACADE); return authFacade.isLoggedIn$.pipe( take(1), diff --git a/src/app/auth/guards/no-auth.guard.ts b/src/app/auth/guards/no-auth.guard.ts index 6c12385..9bf4316 100644 --- a/src/app/auth/guards/no-auth.guard.ts +++ b/src/app/auth/guards/no-auth.guard.ts @@ -2,10 +2,10 @@ import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, createUrlTreeFromSnapshot } from '@angular/router'; import { map, take } from 'rxjs/operators'; -import { AuthFacade } from '../store/auth.facade'; +import { AUTH_FACADE } from '../tokens'; export const noAuthGuard = (route: ActivatedRouteSnapshot) => { - const authFacade = inject(AuthFacade); + const authFacade = inject(AUTH_FACADE); return authFacade.isLoggedIn$.pipe( take(1), diff --git a/src/app/auth/index.ts b/src/app/auth/index.ts index d81f937..d7eb9e0 100644 --- a/src/app/auth/index.ts +++ b/src/app/auth/index.ts @@ -1,20 +1,5 @@ -import { provideEffects } from '@ngrx/effects'; -import { provideState } from '@ngrx/store'; - -import { authServiceInitProvider } from './auth.service'; -import { authInterceptorProviders } from './interceptors'; -import { AuthEffects } from './store/auth.effects'; -import { AUTH_FEATURE_KEY, authReducer } from './store/auth.reducer'; - -export type { AuthUser } from './store/auth.models'; -export { AuthFacade } from './store/auth.facade'; +export type { AuthUser, IAuthFacade } from './models'; +export { AUTH_FACADE } from './tokens'; export { authGuard } from './guards'; - -export function provideAuthStore() { - return [ - provideState(AUTH_FEATURE_KEY, authReducer), - provideEffects(AuthEffects), - authServiceInitProvider, - authInterceptorProviders, - ]; -} +export { authInterceptor } from './interceptors'; +export { provideAuthStore } from './store/index.ngrx'; diff --git a/src/app/auth/interceptors/auth.interceptor.ts b/src/app/auth/interceptors/auth.interceptor.ts index 586c0e0..e910fab 100644 --- a/src/app/auth/interceptors/auth.interceptor.ts +++ b/src/app/auth/interceptors/auth.interceptor.ts @@ -1,57 +1,53 @@ import { HttpErrorResponse, HttpEvent, - HttpHandler, HttpRequest, - HttpInterceptor, + HttpHandlerFn, } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject } from '@angular/core'; import { EMPTY, Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { TokenStorageService } from '../../core/services'; -import { AuthFacade } from '../store/auth.facade'; +import { AUTH_FACADE } from '../tokens'; -@Injectable() -export class AuthInterceptor implements HttpInterceptor { - private readonly authFacade = inject(AuthFacade); - private readonly tokenStorageService = inject(TokenStorageService); +export function authInterceptor( + req: HttpRequest, + next: HttpHandlerFn +): Observable> { + const authFacade = inject(AUTH_FACADE); + const tokenStorageService = inject(TokenStorageService); - intercept( - req: HttpRequest, - next: HttpHandler - ): Observable> { - const accessToken = this.tokenStorageService.getAccessToken(); - - if (accessToken) { - // Add the Authorization header to the request - req = req.clone({ setHeaders: { Authorization: `Bearer ${accessToken}` } }); - } + const handle401 = () => { + authFacade.logout(); + return EMPTY; + }; - return next.handle(req).pipe( - catchError((error: HttpErrorResponse) => { - // try to avoid errors on logout - // therefore we check the url path of '/auth/' - const ignoreAPIs = ['/auth/']; - if (ignoreAPIs.some(api => req.url.includes(api))) { - return throwError(() => error); - } + const accessToken = tokenStorageService.getAccessToken(); - // Handle global error status - switch (error.status) { - case 401: - return this.handle401(); - // Add more error status handling here (e.g. 403) - default: - // Rethrow the error as is - return throwError(() => error); - } - }) - ); + if (accessToken) { + // Add the Authorization header to the request + req = req.clone({ setHeaders: { Authorization: `Bearer ${accessToken}` } }); } - private handle401() { - this.authFacade.logout(); - return EMPTY; - } + return next(req).pipe( + catchError((error: HttpErrorResponse) => { + // try to avoid errors on logout + // therefore we check the url path of '/auth/' + const ignoreAPIs = ['/auth/']; + if (ignoreAPIs.some(api => req.url.includes(api))) { + return throwError(() => error); + } + + // Handle global error status + switch (error.status) { + case 401: + return handle401(); + // Add more error status handling here (e.g. 403) + default: + // Rethrow the error as is + return throwError(() => error); + } + }) + ); } diff --git a/src/app/auth/interceptors/index.ts b/src/app/auth/interceptors/index.ts index e969ec9..25ab787 100644 --- a/src/app/auth/interceptors/index.ts +++ b/src/app/auth/interceptors/index.ts @@ -1,8 +1 @@ -import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { Provider } from '@angular/core'; - -import { AuthInterceptor } from './auth.interceptor'; - -export const authInterceptorProviders: Provider[] = [ - { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, -]; +export { authInterceptor } from './auth.interceptor'; diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts index 02120d5..6728144 100644 --- a/src/app/auth/login/login.component.ts +++ b/src/app/auth/login/login.component.ts @@ -7,7 +7,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { combineLatest } from 'rxjs'; -import { AuthFacade } from '../store/auth.facade'; +import { AUTH_FACADE } from '../tokens'; @Component({ selector: 'aa-login', @@ -24,7 +24,7 @@ import { AuthFacade } from '../store/auth.facade'; styleUrls: ['./login.component.scss'], }) export class LoginComponent { - private readonly authFacade = inject(AuthFacade); + private readonly authFacade = inject(AUTH_FACADE); readonly loginForm = new FormGroup({ username: new FormControl('', { diff --git a/src/app/auth/models/auth-facade.model.ts b/src/app/auth/models/auth-facade.model.ts new file mode 100644 index 0000000..eca03b1 --- /dev/null +++ b/src/app/auth/models/auth-facade.model.ts @@ -0,0 +1,14 @@ +import { Observable } from 'rxjs'; + +import { AuthUser } from './auth-user.model'; + +export interface IAuthFacade { + readonly authUser$: Observable; + readonly isLoggedIn$: Observable; + readonly isLoadingLogin$: Observable; + readonly hasLoginError$: Observable; + + login(username: string, password: string): void; + logout(): void; + getAuthUser(): void; +} diff --git a/src/app/auth/models/auth-user.model.ts b/src/app/auth/models/auth-user.model.ts new file mode 100644 index 0000000..558246e --- /dev/null +++ b/src/app/auth/models/auth-user.model.ts @@ -0,0 +1,5 @@ +export interface AuthUser { + id: number; + firstName: string; + lastName: string; +} diff --git a/src/app/auth/models/index.ts b/src/app/auth/models/index.ts new file mode 100644 index 0000000..23fd9b4 --- /dev/null +++ b/src/app/auth/models/index.ts @@ -0,0 +1,2 @@ +export * from './auth-facade.model'; +export * from './auth-user.model'; diff --git a/src/app/auth/store/index.ngrx.ts b/src/app/auth/store/index.ngrx.ts new file mode 100644 index 0000000..4bf206d --- /dev/null +++ b/src/app/auth/store/index.ngrx.ts @@ -0,0 +1,27 @@ +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { provideEffects } from '@ngrx/effects'; +import { provideState } from '@ngrx/store'; + +import { AUTH_FACADE } from '../tokens'; + +import { + provideAuthInit, + AuthEffects, + AUTH_FEATURE_KEY, + authReducer, + NgrxAuthFacade, +} from './ngrx'; + +export function provideAuthStore(): EnvironmentProviders { + return makeEnvironmentProviders([ + // Register Auth Store + provideState(AUTH_FEATURE_KEY, authReducer), + provideEffects(AuthEffects), + provideAuthInit(), + // Register Auth Facade + { + provide: AUTH_FACADE, + useClass: NgrxAuthFacade, + }, + ]); +} diff --git a/src/app/auth/store/auth.actions.ts b/src/app/auth/store/ngrx/auth.actions.ts similarity index 95% rename from src/app/auth/store/auth.actions.ts rename to src/app/auth/store/ngrx/auth.actions.ts index aa96ea1..9258eeb 100644 --- a/src/app/auth/store/auth.actions.ts +++ b/src/app/auth/store/ngrx/auth.actions.ts @@ -1,6 +1,6 @@ import { createAction, createActionGroup, emptyProps, props } from '@ngrx/store'; -import { AuthUser } from './auth.models'; +import { AuthUser } from '../../models'; // Login export const LoginActions = createActionGroup({ diff --git a/src/app/auth/store/auth.effects.ts b/src/app/auth/store/ngrx/auth.effects.ts similarity index 96% rename from src/app/auth/store/auth.effects.ts rename to src/app/auth/store/ngrx/auth.effects.ts index 872823a..14a656f 100644 --- a/src/app/auth/store/auth.effects.ts +++ b/src/app/auth/store/ngrx/auth.effects.ts @@ -4,8 +4,8 @@ import { Actions, createEffect, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, exhaustMap, finalize, map, tap } from 'rxjs/operators'; -import { TokenStorageService } from '../../core/services'; -import { AuthService } from '../auth.service'; +import { TokenStorageService } from '../../../core/services'; +import { AuthService } from '../../auth.service'; import { AuthUserActions, diff --git a/src/app/auth/store/auth.facade.ts b/src/app/auth/store/ngrx/auth.facade.ts similarity index 88% rename from src/app/auth/store/auth.facade.ts rename to src/app/auth/store/ngrx/auth.facade.ts index 80d3517..ceadc04 100644 --- a/src/app/auth/store/auth.facade.ts +++ b/src/app/auth/store/ngrx/auth.facade.ts @@ -1,11 +1,13 @@ import { Injectable, inject } from '@angular/core'; import { Store } from '@ngrx/store'; +import { IAuthFacade } from '../../models'; + import { LogoutAction, LoginActions, AuthUserActions } from './auth.actions'; import * as AuthSelectors from './auth.selectors'; -@Injectable({ providedIn: 'root' }) -export class AuthFacade { +@Injectable() +export class NgrxAuthFacade implements IAuthFacade { private readonly store = inject(Store); readonly authUser$ = this.store.select(AuthSelectors.selectAuthUser); diff --git a/src/app/auth/store/auth.models.ts b/src/app/auth/store/ngrx/auth.models.ts similarity index 78% rename from src/app/auth/store/auth.models.ts rename to src/app/auth/store/ngrx/auth.models.ts index 13199e6..2eebf39 100644 --- a/src/app/auth/store/auth.models.ts +++ b/src/app/auth/store/ngrx/auth.models.ts @@ -1,3 +1,5 @@ +import { AuthUser } from '../../models'; + export enum TokenStatus { PENDING = 'PENDING', VALIDATING = 'VALIDATING', @@ -13,9 +15,3 @@ export interface AuthState { isLoadingLogin: boolean; hasLoginError: boolean; } - -export interface AuthUser { - id: number; - firstName: string; - lastName: string; -} diff --git a/src/app/auth/store/auth.reducer.ts b/src/app/auth/store/ngrx/auth.reducer.ts similarity index 100% rename from src/app/auth/store/auth.reducer.ts rename to src/app/auth/store/ngrx/auth.reducer.ts diff --git a/src/app/auth/store/auth.selectors.ts b/src/app/auth/store/ngrx/auth.selectors.ts similarity index 100% rename from src/app/auth/store/auth.selectors.ts rename to src/app/auth/store/ngrx/auth.selectors.ts diff --git a/src/app/auth/store/ngrx/index.ts b/src/app/auth/store/ngrx/index.ts new file mode 100644 index 0000000..277f9e0 --- /dev/null +++ b/src/app/auth/store/ngrx/index.ts @@ -0,0 +1,33 @@ +import { EnvironmentProviders, inject, provideAppInitializer } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { lastValueFrom } from 'rxjs'; +import { filter, take } from 'rxjs/operators'; + +import { RefreshTokenActions } from './auth.actions'; +import { AuthState, TokenStatus } from './auth.models'; +import * as AuthSelectors from './auth.selectors'; + +export { AuthEffects } from './auth.effects'; +export { NgrxAuthFacade } from './auth.facade'; +export * from './auth.reducer'; + +const initializeAuth = () => { + const store = inject>(Store); + + store.dispatch(RefreshTokenActions.request()); + + const authState$ = store.select(AuthSelectors.selectAuth).pipe( + filter( + auth => + auth.refreshTokenStatus === TokenStatus.INVALID || + (auth.refreshTokenStatus === TokenStatus.VALID && !!auth.user) + ), + take(1) + ); + + return lastValueFrom(authState$); +}; + +export const provideAuthInit = (): EnvironmentProviders => { + return provideAppInitializer(initializeAuth); +}; diff --git a/src/app/auth/tokens/auth-facade.token.ts b/src/app/auth/tokens/auth-facade.token.ts new file mode 100644 index 0000000..a27c661 --- /dev/null +++ b/src/app/auth/tokens/auth-facade.token.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; + +import { IAuthFacade } from '../models'; + +export const AUTH_FACADE = new InjectionToken('AUTH_FACADE'); diff --git a/src/app/auth/tokens/index.ts b/src/app/auth/tokens/index.ts new file mode 100644 index 0000000..3b1ee09 --- /dev/null +++ b/src/app/auth/tokens/index.ts @@ -0,0 +1 @@ +export * from './auth-facade.token'; diff --git a/src/app/features/secured-feat/secured-feat.component.ts b/src/app/features/secured-feat/secured-feat.component.ts index 6c551d1..e3a43cd 100644 --- a/src/app/features/secured-feat/secured-feat.component.ts +++ b/src/app/features/secured-feat/secured-feat.component.ts @@ -4,7 +4,7 @@ import { MatCardModule } from '@angular/material/card'; import { MatTableModule } from '@angular/material/table'; import { combineLatest, of } from 'rxjs'; -import { AuthFacade } from '../../auth'; +import { AUTH_FACADE } from '../../auth'; import { USERS } from '../../core/fake-api'; import { GreetingUtil } from '../../shared/util'; @Component({ @@ -14,7 +14,7 @@ import { GreetingUtil } from '../../shared/util'; templateUrl: './secured-feat.component.html', }) export class SecuredFeatComponent { - private readonly authFacade = inject(AuthFacade); + private readonly authFacade = inject(AUTH_FACADE); readonly displayedColumns: string[] = ['id', 'name', 'username', 'password']; From 03acbaf50ced2eef6d9228101a721ee24fc9fba5 Mon Sep 17 00:00:00 2001 From: Nikos Anifantis Date: Fri, 14 Feb 2025 09:43:53 +0200 Subject: [PATCH 2/5] chore(store): install and setup ngxs [WIP] --- package-lock.json | 37 +++++++++++++++++++++++ package.json | 2 ++ src/app/app.config.ts | 27 +++++------------ src/app/app.store.ts | 50 ++++++++++++++++++++++++++++++++ src/app/auth/index.ts | 5 +++- src/app/auth/store/index.ngxs.ts | 7 +++++ 6 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 src/app/app.store.ts create mode 100644 src/app/auth/store/index.ngxs.ts diff --git a/package-lock.json b/package-lock.json index 3cded8c..9b44543 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,8 @@ "@ngrx/router-store": "^19.0.1", "@ngrx/store": "^19.0.1", "@ngrx/store-devtools": "^19.0.1", + "@ngxs/devtools-plugin": "19.0.0", + "@ngxs/store": "^19.0.0", "rxjs": "^7.8.1", "tailwindcss": "^3.4.14", "tslib": "^2.3.1" @@ -2887,6 +2889,41 @@ "rxjs": "^6.5.3 || ^7.5.0" } }, + "node_modules/@ngxs/devtools-plugin": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@ngxs/devtools-plugin/-/devtools-plugin-19.0.0.tgz", + "integrity": "sha512-z3O/G0fGeSc/mQRMBWwQ98W+kB0QpIMPZg2FLIubyZwWydouVatjhYck4IDLR/h5i6lq4McKioMK2tn/mXZqnQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ngxs" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0 <20.0.0", + "@ngxs/store": "^19.0.0 || ^19.0.0-dev", + "rxjs": ">=6.5.5" + } + }, + "node_modules/@ngxs/store": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@ngxs/store/-/store-19.0.0.tgz", + "integrity": "sha512-h8xMl3OisrYabdfbUQjy98X/BSaId8t0iX3VlQgOmG0sYuC5OuZvggZywn0urLkA3H97LEI7ihvuS8spEBo6YA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ngxs" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0 <20.0.0", + "rxjs": ">=7.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index fec462a..d288a5d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "@ngrx/router-store": "^19.0.1", "@ngrx/store": "^19.0.1", "@ngrx/store-devtools": "^19.0.1", + "@ngxs/devtools-plugin": "19.0.0", + "@ngxs/store": "^19.0.0", "rxjs": "^7.8.1", "tailwindcss": "^3.4.14", "tslib": "^2.3.1" diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 5f14d2f..241028e 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -5,22 +5,14 @@ import { } from '@angular/core'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideRouter } from '@angular/router'; -import { provideEffects } from '@ngrx/effects'; -import { provideRouterStore, routerReducer } from '@ngrx/router-store'; -import { provideStore } from '@ngrx/store'; -import { provideStoreDevtools } from '@ngrx/store-devtools'; - -import { environment } from '../environments/environment'; import { routes } from './app.routes'; -import { provideAuthStore, authInterceptor } from './auth'; +import { provideAuthStore, provideSetupStore, StoreType } from './app.store'; +import { authInterceptor } from './auth'; import { fakeApiInterceptor } from './core/fake-api'; -function provideAppDevTools() { - return environment.production - ? [] - : [provideStoreDevtools({ name: 'Angular Authentication' })]; -} +// TODO: implement it and describe further steps (e.g. uninstall store packages) +const storeType = StoreType.Ngxs; export const appConfig: ApplicationConfig = { providers: [ @@ -28,10 +20,8 @@ export const appConfig: ApplicationConfig = { provideExperimentalZonelessChangeDetection(), provideAnimationsAsync(), - // Setup NgRx - provideStore({ router: routerReducer }), - provideRouterStore(), - provideEffects(), + // Setup Store + provideSetupStore(storeType), // Setup Interceptors provideHttpClient( @@ -43,10 +33,7 @@ export const appConfig: ApplicationConfig = { ), // Setup Application - provideAuthStore(), + provideAuthStore(storeType), provideRouter(routes), - - // Setup DevTools - provideAppDevTools(), ], }; diff --git a/src/app/app.store.ts b/src/app/app.store.ts new file mode 100644 index 0000000..b77757d --- /dev/null +++ b/src/app/app.store.ts @@ -0,0 +1,50 @@ +import { provideEffects } from '@ngrx/effects'; +import { provideRouterStore, routerReducer } from '@ngrx/router-store'; +import { provideStore as provideNgrxStore } from '@ngrx/store'; +import { provideStoreDevtools } from '@ngrx/store-devtools'; +import { withNgxsReduxDevtoolsPlugin } from '@ngxs/devtools-plugin'; +import { provideStore as provideNgxsStore } from '@ngxs/store'; + +import { environment } from '../environments/environment'; + +import { provideNgrxAuthStore, provideNgxsAuthStore } from './auth'; + +export enum StoreType { + Ngrx = 'ngrx', + Ngxs = 'ngxs', +} + +/** + * Provides all the necessary store providers for setting up the store. + * It supports both Ngrx and Ngxs. + */ +export function provideSetupStore(storeType: StoreType) { + const isDevToolsEnabled = !environment.production; + + const providers = { + ngrx: [ + provideNgrxStore({ router: routerReducer }), + provideRouterStore(), + provideEffects(), + isDevToolsEnabled ? provideStoreDevtools({ name: 'Angular Authentication' }) : [], + ], + ngxs: [ + provideNgxsStore([], ...(isDevToolsEnabled ? [withNgxsReduxDevtoolsPlugin()] : [])), + ], + }; + + return providers[storeType]; +} + +/** + * Provides the authentication store for the application. + * It supports both Ngrx and Ngxs. + */ +export function provideAuthStore(storeType: StoreType) { + const providers = { + ngrx: provideNgrxAuthStore(), + ngxs: provideNgxsAuthStore(), + }; + + return providers[storeType]; +} diff --git a/src/app/auth/index.ts b/src/app/auth/index.ts index d7eb9e0..7b7e4c3 100644 --- a/src/app/auth/index.ts +++ b/src/app/auth/index.ts @@ -2,4 +2,7 @@ export type { AuthUser, IAuthFacade } from './models'; export { AUTH_FACADE } from './tokens'; export { authGuard } from './guards'; export { authInterceptor } from './interceptors'; -export { provideAuthStore } from './store/index.ngrx'; + +// Stores +export { provideAuthStore as provideNgrxAuthStore } from './store/index.ngrx'; +export { provideAuthStore as provideNgxsAuthStore } from './store/index.ngxs'; diff --git a/src/app/auth/store/index.ngxs.ts b/src/app/auth/store/index.ngxs.ts new file mode 100644 index 0000000..0099058 --- /dev/null +++ b/src/app/auth/store/index.ngxs.ts @@ -0,0 +1,7 @@ +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; + +export function provideAuthStore(): EnvironmentProviders { + return makeEnvironmentProviders([ + // TODO: implement it + ]); +} From 3dc7b6365f1104c25d20a35dbcce2980f9fd862e Mon Sep 17 00:00:00 2001 From: Nikos Anifantis Date: Fri, 14 Feb 2025 15:04:21 +0200 Subject: [PATCH 3/5] feat(store): implement `ngxs` as alternative store --- README.md | 38 +++++- src/app/app.config.ts | 2 +- src/app/app.store.ts | 27 +++- .../auth-state.model.ts} | 4 +- src/app/auth/models/index.ts | 1 + src/app/auth/store/index.ngxs.ts | 13 +- src/app/auth/store/ngrx/auth.reducer.ts | 26 ++-- src/app/auth/store/ngrx/auth.selectors.ts | 5 +- src/app/auth/store/ngrx/index.ts | 5 +- src/app/auth/store/ngxs/auth.actions.ts | 20 +++ src/app/auth/store/ngxs/auth.facade.ts | 29 ++++ src/app/auth/store/ngxs/auth.selectors.ts | 32 +++++ src/app/auth/store/ngxs/auth.state.ts | 126 ++++++++++++++++++ src/app/auth/store/ngxs/index.ts | 32 +++++ src/app/features/about/about.component.html | 2 +- src/app/features/home/features.data.ts | 7 + 16 files changed, 340 insertions(+), 29 deletions(-) rename src/app/auth/{store/ngrx/auth.models.ts => models/auth-state.model.ts} (78%) create mode 100644 src/app/auth/store/ngxs/auth.actions.ts create mode 100644 src/app/auth/store/ngxs/auth.facade.ts create mode 100644 src/app/auth/store/ngxs/auth.selectors.ts create mode 100644 src/app/auth/store/ngxs/auth.state.ts create mode 100644 src/app/auth/store/ngxs/index.ts diff --git a/README.md b/README.md index be43e06..ee3b137 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Live application: [angular-authentication.netlify.app](https://angular-authentic - [Node.js](https://nodejs.org/en/) - [Angular CLI](https://angular.io/cli) -### Setup & Usage +### Setup & Local Development - Clone this repository: `git clone git@github.com:nikosanif/angular-authentication.git` - `cd angular-authentication` @@ -41,6 +41,25 @@ Live application: [angular-authentication.netlify.app](https://angular-authentic - Serve the Angular app: `npm start` - Open your browser at: `http://localhost:4200` +### Use it as a Template + +The main purpose of this repository is to provide a simple Angular application that demonstrates best practices for user authentication and authorization flows. The application is configured to use a fake API server (interceptor) that simulates the backend server. Also, it includes two state management libraries, NgRx and NGXS, so you can choose which one to use. + +If you want to use this repository as a template for your project, you can follow these steps: + +- Clone this repository +- Remove fake API: + - Delete `src/app/core/fake-api` folder + - Remove all references from the `fake-api` folder + - Remove the `fakeApiInterceptor` from `app.config.ts` +- Choose the state management library you want to use: + - NgRx: Remove `src/app/auth/store/ngxs` folder and the `index.ngxs.ts` file + - NGXS: Remove `src/app/auth/store/ngrx` folder and the `index.ngrx.ts` file + - Rename the `index.XXX.ts` file to `index.ts` in the `src/app/auth/store` folder + - Update the `app.store.ts` file to import the correct store module + - Remove all unused packages from `package.json` +- Update the Google Analytics tracking ID by replacing `UA-XXXXX-Y` in the `index.html` file and in the `src/app/core/services/google-analytics.service.ts` file. Or remove the Google Analytics service if you don't want to use it. + ### Useful Commands - `npm start` - starts a dev server of Angular app @@ -63,14 +82,16 @@ Live application: [angular-authentication.netlify.app](https://angular-authentic - Standalone Angular components - Angular Material UI components - Lazy loading of Angular components -- API requests with `@ngrx/effects` +- API requests with `@ngrx/effects` or `@ngxs/store` (you can choose at `src/app/app.config.ts`) - Responsive design - Custom In-memory Web API using interceptors ## Tech Stack - [Angular](https://angular.io/) -- [NgRX](https://ngrx.io/) - @ngrx/{store,effects,component} +- State Management. This repos demonstrates **two** state management libraries, you can choose which one to use by following the instructions in the [Use it as a Template](#use-it-as-a-template) section. + - [NgRX](https://ngrx.io/) - @ngrx/{store,effects,component} + - [NGXS](https://www.ngxs.io/) - @ngxs/store - [Angular Material UI](https://material.angular.io/) - [Tailwind CSS](https://tailwindcss.com/) - Other dev tools @@ -90,15 +111,20 @@ Below is the high-level structure of the application. │ ├── app.component.ts │ ├── app.config.ts │ ├── app.routes.ts +│ ├── app.store.ts # configure store based on NgRx or NGXS │ │ │ ├── auth # includes authentication logic │ │ ├── auth.routes.ts │ │ ├── auth.service.ts -│ │ ├── guards │ │ ├── index.ts +│ │ ├── guards │ │ ├── interceptors │ │ ├── login -│ │ └── store +│ │ ├── models +│ │ ├── tokens +│ │ └── store # Choose one of the following +│ │ ├── ngrx # store based on NgRx +│ │ └── ngxs # store based on NGXS │ │ │ ├── core # includes core utilities │ │ ├── fake-api @@ -139,7 +165,7 @@ If you have found any bug in the source code or want to _request_ a new feature, ## Support - Star this repository 👆⭐️ -- Help it spread to a wider audience: [![X](https://img.shields.io/twitter/url/https/x.com/nikosanif.svg?style=social&label=Tweet)](https://x.com/intent/tweet?text=An%20Angular%20application%20that%20demonstrates%20best%20practices%20for%20user%20authentication%20and%20authorization%20flows.%0A%0A%40nikosanif%20%0A%F0%9F%94%97%20https%3A%2F%2Fgithub.com%2Fnikosanif%2Fangular-authentication%0A%0A&hashtags=Angular,NgRx,MDX,tailwindcss,ngAuth) +- Help it spread to a wider audience: [![X](https://img.shields.io/twitter/url/https/x.com/nikosanif.svg?style=social&label=Tweet)](https://x.com/intent/tweet?text=An%20Angular%20application%20that%20demonstrates%20best%20practices%20for%20user%20authentication%20and%20authorization%20flows.%0A%0A%40nikosanif%20%0A%F0%9F%94%97%20https%3A%2F%2Fgithub.com%2Fnikosanif%2Fangular-authentication%0A%0A&hashtags=Angular,NgRx,NGXS,MDX,tailwindcss,ngAuth) ### Author: Nikos Anifantis ✍️ diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 241028e..c75a705 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -11,7 +11,7 @@ import { provideAuthStore, provideSetupStore, StoreType } from './app.store'; import { authInterceptor } from './auth'; import { fakeApiInterceptor } from './core/fake-api'; -// TODO: implement it and describe further steps (e.g. uninstall store packages) +// ⚠️ FIXME: choose one store and remove any packages in real app ⚠️ const storeType = StoreType.Ngxs; export const appConfig: ApplicationConfig = { diff --git a/src/app/app.store.ts b/src/app/app.store.ts index b77757d..d84ce10 100644 --- a/src/app/app.store.ts +++ b/src/app/app.store.ts @@ -1,14 +1,26 @@ +/** + * ⚠️ FIXME: choose one store and remove any unused packages in real app ⚠️ + * This file contains the store setup for the application. + * It supports both Ngrx and Ngxs for the store management, but only one can be used at a time. + * In real applications, you should choose one and remove the unused packages. + */ + import { provideEffects } from '@ngrx/effects'; import { provideRouterStore, routerReducer } from '@ngrx/router-store'; import { provideStore as provideNgrxStore } from '@ngrx/store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; import { withNgxsReduxDevtoolsPlugin } from '@ngxs/devtools-plugin'; -import { provideStore as provideNgxsStore } from '@ngxs/store'; +import { + provideStore as provideNgxsStore, + withNgxsDevelopmentOptions, +} from '@ngxs/store'; import { environment } from '../environments/environment'; import { provideNgrxAuthStore, provideNgxsAuthStore } from './auth'; +const APP_NAME = 'Angular Authentication'; + export enum StoreType { Ngrx = 'ngrx', Ngxs = 'ngxs', @@ -26,10 +38,19 @@ export function provideSetupStore(storeType: StoreType) { provideNgrxStore({ router: routerReducer }), provideRouterStore(), provideEffects(), - isDevToolsEnabled ? provideStoreDevtools({ name: 'Angular Authentication' }) : [], + isDevToolsEnabled ? provideStoreDevtools({ name: APP_NAME }) : [], ], ngxs: [ - provideNgxsStore([], ...(isDevToolsEnabled ? [withNgxsReduxDevtoolsPlugin()] : [])), + provideNgxsStore( + [], + withNgxsReduxDevtoolsPlugin({ + name: APP_NAME, + disabled: !isDevToolsEnabled, + }), + withNgxsDevelopmentOptions({ + warnOnUnhandledActions: true, + }) + ), ], }; diff --git a/src/app/auth/store/ngrx/auth.models.ts b/src/app/auth/models/auth-state.model.ts similarity index 78% rename from src/app/auth/store/ngrx/auth.models.ts rename to src/app/auth/models/auth-state.model.ts index 2eebf39..66678af 100644 --- a/src/app/auth/store/ngrx/auth.models.ts +++ b/src/app/auth/models/auth-state.model.ts @@ -1,4 +1,4 @@ -import { AuthUser } from '../../models'; +import { AuthUser } from './auth-user.model'; export enum TokenStatus { PENDING = 'PENDING', @@ -7,7 +7,7 @@ export enum TokenStatus { INVALID = 'INVALID', } -export interface AuthState { +export interface AuthStateModel { isLoggedIn: boolean; user?: AuthUser; accessTokenStatus: TokenStatus; diff --git a/src/app/auth/models/index.ts b/src/app/auth/models/index.ts index 23fd9b4..80b38c7 100644 --- a/src/app/auth/models/index.ts +++ b/src/app/auth/models/index.ts @@ -1,2 +1,3 @@ export * from './auth-facade.model'; +export * from './auth-state.model'; export * from './auth-user.model'; diff --git a/src/app/auth/store/index.ngxs.ts b/src/app/auth/store/index.ngxs.ts index 0099058..6ae5322 100644 --- a/src/app/auth/store/index.ngxs.ts +++ b/src/app/auth/store/index.ngxs.ts @@ -1,7 +1,18 @@ import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { provideStates } from '@ngxs/store'; + +import { AUTH_FACADE } from '../tokens'; + +import { AuthState, NgxsAuthFacade, provideAuthInit } from './ngxs'; export function provideAuthStore(): EnvironmentProviders { return makeEnvironmentProviders([ - // TODO: implement it + provideStates([AuthState]), + provideAuthInit(), + // Register Auth Facade + { + provide: AUTH_FACADE, + useClass: NgxsAuthFacade, + }, ]); } diff --git a/src/app/auth/store/ngrx/auth.reducer.ts b/src/app/auth/store/ngrx/auth.reducer.ts index 58a121b..9dda096 100644 --- a/src/app/auth/store/ngrx/auth.reducer.ts +++ b/src/app/auth/store/ngrx/auth.reducer.ts @@ -1,20 +1,21 @@ import { Action, createReducer, on } from '@ngrx/store'; +import { AuthStateModel, TokenStatus } from '../../models'; + import { AuthUserActions, LoginActions, LogoutAction, RefreshTokenActions, } from './auth.actions'; -import { AuthState, TokenStatus } from './auth.models'; export const AUTH_FEATURE_KEY = 'auth'; export interface AuthPartialState { - readonly [AUTH_FEATURE_KEY]: AuthState; + readonly [AUTH_FEATURE_KEY]: AuthStateModel; } -export const initialState: AuthState = { +export const initialState: AuthStateModel = { isLoggedIn: false, user: undefined, accessTokenStatus: TokenStatus.PENDING, @@ -29,7 +30,7 @@ const reducer = createReducer( // Login on( LoginActions.request, - (state): AuthState => ({ + (state): AuthStateModel => ({ ...state, accessTokenStatus: TokenStatus.VALIDATING, isLoadingLogin: true, @@ -40,7 +41,7 @@ const reducer = createReducer( // Refresh token on( RefreshTokenActions.request, - (state): AuthState => ({ + (state): AuthStateModel => ({ ...state, refreshTokenStatus: TokenStatus.VALIDATING, }) @@ -50,7 +51,7 @@ const reducer = createReducer( on( LoginActions.success, RefreshTokenActions.success, - (state): AuthState => ({ + (state): AuthStateModel => ({ ...state, isLoggedIn: true, isLoadingLogin: false, @@ -61,7 +62,7 @@ const reducer = createReducer( on( LoginActions.failure, RefreshTokenActions.failure, - (state, action): AuthState => ({ + (state, action): AuthStateModel => ({ ...state, isLoadingLogin: false, accessTokenStatus: TokenStatus.INVALID, @@ -73,7 +74,7 @@ const reducer = createReducer( // Logout on( LogoutAction, - (): AuthState => ({ + (): AuthStateModel => ({ ...initialState, }) ), @@ -81,19 +82,22 @@ const reducer = createReducer( // Auth user on( AuthUserActions.success, - (state, action): AuthState => ({ + (state, action): AuthStateModel => ({ ...state, user: action.user, }) ), on( AuthUserActions.failure, - (): AuthState => ({ + (): AuthStateModel => ({ ...initialState, }) ) ); -export function authReducer(state: AuthState | undefined, action: Action): AuthState { +export function authReducer( + state: AuthStateModel | undefined, + action: Action +): AuthStateModel { return reducer(state, action); } diff --git a/src/app/auth/store/ngrx/auth.selectors.ts b/src/app/auth/store/ngrx/auth.selectors.ts index 5f615ae..3f246d0 100644 --- a/src/app/auth/store/ngrx/auth.selectors.ts +++ b/src/app/auth/store/ngrx/auth.selectors.ts @@ -1,9 +1,10 @@ import { createFeatureSelector, createSelector } from '@ngrx/store'; -import { AuthState } from './auth.models'; +import { AuthStateModel } from '../../models'; + import { AUTH_FEATURE_KEY } from './auth.reducer'; -export const selectAuth = createFeatureSelector(AUTH_FEATURE_KEY); +export const selectAuth = createFeatureSelector(AUTH_FEATURE_KEY); export const selectIsLoggedIn = createSelector(selectAuth, state => state.isLoggedIn); diff --git a/src/app/auth/store/ngrx/index.ts b/src/app/auth/store/ngrx/index.ts index 277f9e0..9d47229 100644 --- a/src/app/auth/store/ngrx/index.ts +++ b/src/app/auth/store/ngrx/index.ts @@ -3,8 +3,9 @@ import { Store } from '@ngrx/store'; import { lastValueFrom } from 'rxjs'; import { filter, take } from 'rxjs/operators'; +import { AuthStateModel, TokenStatus } from '../../models'; + import { RefreshTokenActions } from './auth.actions'; -import { AuthState, TokenStatus } from './auth.models'; import * as AuthSelectors from './auth.selectors'; export { AuthEffects } from './auth.effects'; @@ -12,7 +13,7 @@ export { NgrxAuthFacade } from './auth.facade'; export * from './auth.reducer'; const initializeAuth = () => { - const store = inject>(Store); + const store = inject>(Store); store.dispatch(RefreshTokenActions.request()); diff --git a/src/app/auth/store/ngxs/auth.actions.ts b/src/app/auth/store/ngxs/auth.actions.ts new file mode 100644 index 0000000..f9034fe --- /dev/null +++ b/src/app/auth/store/ngxs/auth.actions.ts @@ -0,0 +1,20 @@ +export class Login { + static readonly type = '[Auth] Login'; + + constructor( + public username: string, + public password: string + ) {} +} + +export class Logout { + static readonly type = '[Auth] Logout'; +} + +export class FetchAuthUser { + static readonly type = '[Auth] Fetch Auth User'; +} + +export class RefreshToken { + static readonly type = '[Auth] Refresh Token'; +} diff --git a/src/app/auth/store/ngxs/auth.facade.ts b/src/app/auth/store/ngxs/auth.facade.ts new file mode 100644 index 0000000..081c666 --- /dev/null +++ b/src/app/auth/store/ngxs/auth.facade.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { Store } from '@ngxs/store'; + +import { IAuthFacade } from '../../models'; + +import { FetchAuthUser, Login, Logout } from './auth.actions'; +import { AuthSelectors } from './auth.selectors'; + +@Injectable() +export class NgxsAuthFacade implements IAuthFacade { + private readonly store = inject(Store); + + readonly authUser$ = this.store.select(AuthSelectors.authUser); + readonly isLoggedIn$ = this.store.select(AuthSelectors.isLoggedIn); + readonly isLoadingLogin$ = this.store.select(AuthSelectors.isLoadingLogin); + readonly hasLoginError$ = this.store.select(AuthSelectors.loginError); + + login(username: string, password: string) { + this.store.dispatch(new Login(username, password)); + } + + logout() { + this.store.dispatch(new Logout()); + } + + getAuthUser() { + this.store.dispatch(new FetchAuthUser()); + } +} diff --git a/src/app/auth/store/ngxs/auth.selectors.ts b/src/app/auth/store/ngxs/auth.selectors.ts new file mode 100644 index 0000000..f09efd7 --- /dev/null +++ b/src/app/auth/store/ngxs/auth.selectors.ts @@ -0,0 +1,32 @@ +import { Selector } from '@ngxs/store'; + +import { AuthStateModel } from '../../models'; + +import { AuthState } from './auth.state'; + +export class AuthSelectors { + @Selector([AuthState]) + static auth(state: AuthStateModel) { + return state; + } + + @Selector([AuthState]) + static isLoggedIn(state: AuthStateModel) { + return state.isLoggedIn; + } + + @Selector([AuthState]) + static loginError(state: AuthStateModel) { + return state.hasLoginError; + } + + @Selector([AuthState]) + static isLoadingLogin(state: AuthStateModel) { + return state.isLoadingLogin; + } + + @Selector([AuthState]) + static authUser(state: AuthStateModel) { + return state.user; + } +} diff --git a/src/app/auth/store/ngxs/auth.state.ts b/src/app/auth/store/ngxs/auth.state.ts new file mode 100644 index 0000000..cf8d540 --- /dev/null +++ b/src/app/auth/store/ngxs/auth.state.ts @@ -0,0 +1,126 @@ +import { inject, Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Action, State, StateContext } from '@ngxs/store'; +import { catchError, finalize, switchMap, tap, throwError } from 'rxjs'; + +import { TokenStorageService } from '../../../core/services'; +import { AuthService } from '../../auth.service'; +import { AuthStateModel, TokenStatus } from '../../models'; + +import { Login, Logout, RefreshToken, FetchAuthUser } from './auth.actions'; + +const AUTH_FEATURE_KEY = 'auth'; + +const initialState: AuthStateModel = { + isLoggedIn: false, + user: undefined, + accessTokenStatus: TokenStatus.PENDING, + refreshTokenStatus: TokenStatus.PENDING, + isLoadingLogin: false, + hasLoginError: false, +}; + +@State({ + name: AUTH_FEATURE_KEY, + defaults: initialState, +}) +@Injectable() +export class AuthState { + private readonly router = inject(Router); + private readonly authService = inject(AuthService); + private readonly activatedRoute = inject(ActivatedRoute); + private readonly tokenStorageService = inject(TokenStorageService); + + @Action(Login) + login(ctx: StateContext, action: Login) { + ctx.patchState({ + accessTokenStatus: TokenStatus.VALIDATING, + isLoadingLogin: true, + hasLoginError: false, + }); + + return this.authService.login(action.username, action.password).pipe( + switchMap(data => { + this.tokenStorageService.saveTokens(data.access_token, data.refresh_token); + + ctx.patchState({ + isLoggedIn: true, + isLoadingLogin: false, + accessTokenStatus: TokenStatus.VALID, + refreshTokenStatus: TokenStatus.VALID, + }); + + // Redirect to return url or home + const returnUrl = this.activatedRoute.snapshot.queryParams['returnUrl'] || '/'; + this.router.navigateByUrl(returnUrl); + + return ctx.dispatch(new FetchAuthUser()); + }), + catchError(error => { + ctx.patchState({ + isLoadingLogin: false, + accessTokenStatus: TokenStatus.INVALID, + refreshTokenStatus: TokenStatus.INVALID, + hasLoginError: true, + }); + + this.tokenStorageService.removeTokens(); + + return throwError(() => error); + }) + ); + } + + @Action(Logout) + logout(ctx: StateContext) { + ctx.setState({ ...initialState }); + + this.router.navigateByUrl('/'); + + return this.authService + .logout() + .pipe(finalize(() => this.tokenStorageService.removeTokens())); + } + + @Action(FetchAuthUser) + authUserRequest(ctx: StateContext) { + return this.authService.getAuthUser().pipe( + tap(user => ctx.patchState({ user })), + catchError(error => { + ctx.setState({ ...initialState }); + return throwError(() => error); + }) + ); + } + + @Action(RefreshToken) + refreshTokenRequest(ctx: StateContext) { + ctx.patchState({ refreshTokenStatus: TokenStatus.VALIDATING }); + + return this.authService.refreshToken().pipe( + switchMap(data => { + this.tokenStorageService.saveTokens(data.access_token, data.refresh_token); + + ctx.patchState({ + isLoggedIn: true, + isLoadingLogin: false, + accessTokenStatus: TokenStatus.VALID, + refreshTokenStatus: TokenStatus.VALID, + }); + + return ctx.dispatch(new FetchAuthUser()); + }), + catchError(error => { + ctx.patchState({ + isLoadingLogin: false, + accessTokenStatus: TokenStatus.INVALID, + refreshTokenStatus: TokenStatus.INVALID, + }); + + this.tokenStorageService.removeTokens(); + + return throwError(() => error); + }) + ); + } +} diff --git a/src/app/auth/store/ngxs/index.ts b/src/app/auth/store/ngxs/index.ts new file mode 100644 index 0000000..4a5133e --- /dev/null +++ b/src/app/auth/store/ngxs/index.ts @@ -0,0 +1,32 @@ +import { EnvironmentProviders, inject, provideAppInitializer } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { lastValueFrom } from 'rxjs'; +import { filter, take } from 'rxjs/operators'; + +import { TokenStatus } from '../../models'; + +import { RefreshToken } from './auth.actions'; +import { AuthSelectors } from './auth.selectors'; +export { NgxsAuthFacade } from './auth.facade'; +export { AuthState } from './auth.state'; + +const initializeAuth = () => { + const store = inject(Store); + + store.dispatch(new RefreshToken()); + + const authState$ = store.select(AuthSelectors.auth).pipe( + filter( + auth => + auth.refreshTokenStatus === TokenStatus.INVALID || + (auth.refreshTokenStatus === TokenStatus.VALID && !!auth.user) + ), + take(1) + ); + + return lastValueFrom(authState$); +}; + +export const provideAuthInit = (): EnvironmentProviders => { + return provideAppInitializer(initializeAuth); +}; diff --git a/src/app/features/about/about.component.html b/src/app/features/about/about.component.html index 755eab3..186ff1e 100644 --- a/src/app/features/about/about.component.html +++ b/src/app/features/about/about.component.html @@ -30,7 +30,7 @@

Support ❤️🙏

diff --git a/src/app/features/home/features.data.ts b/src/app/features/home/features.data.ts index a375a4f..8b4f765 100644 --- a/src/app/features/home/features.data.ts +++ b/src/app/features/home/features.data.ts @@ -21,6 +21,13 @@ export const features: Feature[] = [ github: 'https://github.com/ngrx/platform', docs: 'https://ngrx.io/docs', }, + { + name: 'NGXS', + description: 'NGXS is a state management pattern + library for Angular.', + link: 'https://www.ngxs.io/', + github: 'https://github.com/ngxs/store', + docs: 'https://www.ngxs.io', + }, { name: 'Standalone Components', description: 'A simplified way to build Angular applications.', From c507bc5cdb0125e065e12e3f706ada9d2da4b97d Mon Sep 17 00:00:00 2001 From: Nikos Anifantis Date: Mon, 17 Feb 2025 11:12:49 +0200 Subject: [PATCH 4/5] chore: update packages --- package-lock.json | 220 +++++++++++++++++++++++++--------------------- package.json | 30 +++---- 2 files changed, 134 insertions(+), 116 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b44543..574a894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,16 @@ "name": "angular-authentication", "version": "2.1.0", "dependencies": { - "@angular/animations": "^19.1.5", - "@angular/cdk": "~19.1.3", - "@angular/common": "^19.1.5", - "@angular/compiler": "^19.1.5", - "@angular/core": "^19.1.5", - "@angular/forms": "^19.1.5", - "@angular/material": "~19.1.3", - "@angular/platform-browser": "^19.1.5", - "@angular/platform-browser-dynamic": "^19.1.5", - "@angular/router": "^19.1.5", + "@angular/animations": "^19.1.6", + "@angular/cdk": "~19.1.4", + "@angular/common": "^19.1.6", + "@angular/compiler": "^19.1.6", + "@angular/core": "^19.1.6", + "@angular/forms": "^19.1.6", + "@angular/material": "~19.1.4", + "@angular/platform-browser": "^19.1.6", + "@angular/platform-browser-dynamic": "^19.1.6", + "@angular/router": "^19.1.6", "@fortawesome/angular-fontawesome": "^1.0.0", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", @@ -34,19 +34,19 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@angular/build": "^19.1.6", - "@angular/cli": "^19.1.6", - "@angular/compiler-cli": "^19.1.5", + "@angular/build": "^19.1.7", + "@angular/cli": "^19.1.7", + "@angular/compiler-cli": "^19.1.6", "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", "@release-it/conventional-changelog": "^10.0.0", - "@types/node": "^22.13.1", + "@types/node": "^22.13.4", "angular-eslint": "^19.1.0", "eslint": "^9.20.1", "eslint-plugin-import": "^2.31.0", "husky": "^9.1.7", "lint-staged": "^15.4.3", - "prettier": "^3.5.0", + "prettier": "^3.5.1", "prettier-plugin-tailwindcss": "^0.6.11", "release-it": "^18.1.2", "typescript": "~5.5.2", @@ -82,12 +82,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1901.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.6.tgz", - "integrity": "sha512-JiMrs3T1A7RyF5bh0PLGKDjTR8sa/kh8w63+dW0azcNok30tKjLjwJRPTpePokWefjmRgfKaf/iZ8yfFBnpGpA==", + "version": "0.1901.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.7.tgz", + "integrity": "sha512-qltyebfbej7joIKZVH8EFfrVDrkw0p9N9ja3A0XeU1sl2vlepHNAQdVm0Os8Vy2XjjyHvT5bXWE3G3/221qEKw==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.6", + "@angular-devkit/core": "19.1.7", "rxjs": "7.8.1" }, "engines": { @@ -97,10 +98,11 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.6.tgz", - "integrity": "sha512-4s1RpYFGb/yP6OZ1dnYmU7maFYdhZS9pnUHKKiL9rSDhUHkX+VZlf9WFFrHv2RMWg+evrrwPtiFOTMBLShUi8g==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.7.tgz", + "integrity": "sha512-q0I6L9KTqyQ7D5M8H+fWLT+yjapvMNb7SRdfU6GzmexO66Dpo83q4HDzuDKIPDF29Yl0ELs9ICJqe9yUXh6yDQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", @@ -124,12 +126,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.6.tgz", - "integrity": "sha512-6ljZSVTFqnk0utnXLLd82wM6nj68984n5gfrpT1PlOff6MHHNH2YCfwNSlwg6Q5UfDxhEDIT9/MTLnXd6znIRQ==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.7.tgz", + "integrity": "sha512-AP6FvhMybCYs3gs+vzEAzSU1K//AFT3SVTRFv+C3WMO5dLeAHeGzM8I2dxD5EHQQtqIE/8apP6CxGrnpA5YlFg==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.6", + "@angular-devkit/core": "19.1.7", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -259,9 +262,10 @@ } }, "node_modules/@angular/animations": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.1.5.tgz", - "integrity": "sha512-jRZgLdSjr94EpBFIyCUZM7YKBi5TO2+J8PKmz7IdNrYNuUaGfy8k816/57Vgmsb18dnpA2Kf7R2AlOpNcDcsOA==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.1.6.tgz", + "integrity": "sha512-iacosz3fygp0AyT57+suVpLChl10xS5RBje09TfQIKHTUY0LWkMspgaK9gwtlflpIhjedPV0UmgRIKhhFcQM1A==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -269,18 +273,19 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.1.5" + "@angular/core": "19.1.6" } }, "node_modules/@angular/build": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.6.tgz", - "integrity": "sha512-6zGdMxMITBj5oVRDKcOL+ufrCSsPLPd5AeRcGkaCYQDshaOmn0UXL4HQylU3nswhVT0dtCd4eDA7fh2dlyVF6A==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.7.tgz", + "integrity": "sha512-22SjHZDTk91JHU5aFVDU2n+xkPolDosRVfsK4zs+RRXQs30LYPH9KCLiUWCYjFbRj7oYvw7sbrs94szo7dWYvw==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1901.6", - "@angular-devkit/core": "19.1.6", + "@angular-devkit/architect": "0.1901.7", + "@angular-devkit/core": "19.1.7", "@babel/core": "7.26.0", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", @@ -319,7 +324,7 @@ "@angular/localize": "^19.0.0", "@angular/platform-server": "^19.0.0", "@angular/service-worker": "^19.0.0", - "@angular/ssr": "^19.1.6", + "@angular/ssr": "^19.1.7", "less": "^4.2.0", "ng-packagr": "^19.0.0", "postcss": "^8.4.0", @@ -425,9 +430,10 @@ } }, "node_modules/@angular/cdk": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.1.3.tgz", - "integrity": "sha512-A8d1V4AU2ZcNnEEwAUp4W1uYdT7EKHZM0PGicVhLyeetwYrpHiLoPioD7sw89TlPuJcd6mS7xV6AnXQ8peOoXg==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.1.4.tgz", + "integrity": "sha512-PyvJ1VbYjW8tVnVHvcasiqI9eNWf8EJnr0in1QWnhpSbpVpVpc4yjbgnu2pTrW9mPo/YjV4pF+qs6E97y9mdYQ==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -441,17 +447,18 @@ } }, "node_modules/@angular/cli": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.6.tgz", - "integrity": "sha512-5H9Ri+YNPBnac/h1wTPQ+9mLSXfT1n99FwCtMVy6YnG+akRqOKFmPWB29hkFQAgfXi/MYIj+rQKv+d/9yWJibQ==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.7.tgz", + "integrity": "sha512-qVEy0R4QKQ2QAGfpj2mPVxRxgOVst+rIgZBtLwf/mrbN9YyzJUaBKvaVslUpOqkvoW9mX5myf0iZkT5NykrIoA==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1901.6", - "@angular-devkit/core": "19.1.6", - "@angular-devkit/schematics": "19.1.6", + "@angular-devkit/architect": "0.1901.7", + "@angular-devkit/core": "19.1.7", + "@angular-devkit/schematics": "19.1.7", "@inquirer/prompts": "7.2.1", "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.1.6", + "@schematics/angular": "19.1.7", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", @@ -474,9 +481,10 @@ } }, "node_modules/@angular/common": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.1.5.tgz", - "integrity": "sha512-8jR3c5IBMlfiiHvrO8Y2z8y9n4Moy4mI7bS0eu3hmI3m5Vvrgd2Z4GCaQ/Dt4wCtFxcgSsVXiF+/H0QbVdwulA==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.1.6.tgz", + "integrity": "sha512-FkuejwbxsOLhcyOgDM/7YEYvMG3tuyOvr+831VzPwMwYp5QO9AUYtn4ffGf698JccbA+Ocw3BAdhPU6i+YZC1A==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -484,14 +492,15 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.1.5", + "@angular/core": "19.1.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.1.5.tgz", - "integrity": "sha512-8dhticSq98qZanbPBqLACykR08eHbh9WyXG4VJB7Ru9465DjOd6sRM3gmGDNvNlohh30S4xJzPhVrzYXmIyqiA==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.1.6.tgz", + "integrity": "sha512-Tl2PFEtnU8UgSqtEKG827xDUGZrErhR6S1JICeV1kbRCYmwQA4hhG25tzi+ifSAOPW7eJiyzP2LWIvOuZkq3Vw==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -499,7 +508,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.1.5" + "@angular/core": "19.1.6" }, "peerDependenciesMeta": { "@angular/core": { @@ -508,10 +517,11 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.1.5.tgz", - "integrity": "sha512-7IHfGklqiTsDYjk2SgOi5sG63gZ60LguT7dhMGtUdy+fUyK0KGofE1w74LwPHQ3huCdu3rBp7HZvC0/IsmiYtA==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.1.6.tgz", + "integrity": "sha512-rTpHC/tfLBj+5a3X+BA/4s2w5T/cHT6x3RgO8CYy7003Musn0/BiqjfE6VCIllQgLaOQRhCcf51T6Kerkzv8Dw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "7.26.0", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -531,14 +541,15 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.1.5", + "@angular/compiler": "19.1.6", "typescript": ">=5.5 <5.8" } }, "node_modules/@angular/core": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.1.5.tgz", - "integrity": "sha512-N4Uh/jRV2Ksj1iBnhIHkB5hzeiF7J9rhUTiztDPaRT7YpFVt2MKiBXrn52HDcKXPaPFrsZBotbZ6oOMdP4rd5g==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.1.6.tgz", + "integrity": "sha512-FD167URT+apxjzj9sG/MzffW5G6YyQiPQ6nrrIoYi9jeY3LYurybuOgvcXrU8PT4Z3+CKMq9k/ZnmrlHU72BpA==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -551,9 +562,10 @@ } }, "node_modules/@angular/forms": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.1.5.tgz", - "integrity": "sha512-MUebiFrIhwB1m9rp8v/tgftsCmcI5OjUjnbsiuDsPp/291qxbsJ3P/wmvmCHYEJOoFxVLEgOjJvFcmYN/VbxLw==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.1.6.tgz", + "integrity": "sha512-uu/76KAwCAcDuhD67Vv78UvOC/tiprtFXOgqNCj0LK8vyFcvPsunb3nF/PtfF9rSHyslXAqxZhME+Ha2tU6Lpw==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -561,22 +573,23 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.1.5", - "@angular/core": "19.1.5", - "@angular/platform-browser": "19.1.5", + "@angular/common": "19.1.6", + "@angular/core": "19.1.6", + "@angular/platform-browser": "19.1.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.1.3.tgz", - "integrity": "sha512-ii19ow7V8fLsgTvnghDBObte8G0I2orgsG+jwR8fdO1Hp+9d+IEeITLvn2sc7qVofkv/DzG4rCTFaLQdOXRWmg==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.1.4.tgz", + "integrity": "sha512-bqliTnUnMiTG6Quvk16epiQPZQB0zV/L2ctPinFcP6NhahcQFfahjabUlgMlfBk5qObolJArJr5HCMiOmpOGIQ==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^19.0.0 || ^20.0.0", - "@angular/cdk": "19.1.3", + "@angular/cdk": "19.1.4", "@angular/common": "^19.0.0 || ^20.0.0", "@angular/core": "^19.0.0 || ^20.0.0", "@angular/forms": "^19.0.0 || ^20.0.0", @@ -585,9 +598,10 @@ } }, "node_modules/@angular/platform-browser": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.5.tgz", - "integrity": "sha512-wqM4OlGncXNmROTS0mpUpnzzG5DsIZi1U0gzQp5bDOknaFFmg2C2ExCi29CwFZfaOeDw135AyXtu4qItfDOW9A==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.6.tgz", + "integrity": "sha512-sfWU+gMpqQ6GYtE3tAfDktftC01NgtqAOKfeCQ/KY2rxRTIxYahenW0Licuzgmd+8AZtmncoZaYX0Fd/5XMqzQ==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -595,9 +609,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "19.1.5", - "@angular/common": "19.1.5", - "@angular/core": "19.1.5" + "@angular/animations": "19.1.6", + "@angular/common": "19.1.6", + "@angular/core": "19.1.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -606,9 +620,10 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.1.5.tgz", - "integrity": "sha512-mW9Ru5C0/Jg+b2/pWfzfkWmFZ6Exn2J2k+6Unv1Vprh6whF4ch7v5AdBaCuLiK5kUPpQQMHhRz7VY+3mb/dgqQ==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.1.6.tgz", + "integrity": "sha512-QedjG7/ctPtzgJ3LcWv4yMcSivKlwcZ8ge8zPe7eu9Ft6mDZZat65gJEjDuvevJoeNbo2dQODFDiyPJNmnNA9A==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -616,16 +631,17 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.1.5", - "@angular/compiler": "19.1.5", - "@angular/core": "19.1.5", - "@angular/platform-browser": "19.1.5" + "@angular/common": "19.1.6", + "@angular/compiler": "19.1.6", + "@angular/core": "19.1.6", + "@angular/platform-browser": "19.1.6" } }, "node_modules/@angular/router": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.1.5.tgz", - "integrity": "sha512-g5JLymyi+/PTIqKcImSUB9ac1g7szMG/jGax3nTXqwMOzWmxZJJIEKlXWmHJYjUyYEhKBdqLPUMa4JbkD+/jnA==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.1.6.tgz", + "integrity": "sha512-TEfw3W5jVodVDMD4krhXGog1THZN3x1yoh2oZmCv3lXg22+pVC6Cp+x3vVExq0mS+g3/6uZwy/3qAYdlzqYjTg==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -633,9 +649,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.1.5", - "@angular/core": "19.1.5", - "@angular/platform-browser": "19.1.5", + "@angular/common": "19.1.6", + "@angular/core": "19.1.6", + "@angular/platform-browser": "19.1.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -3980,13 +3996,14 @@ "dev": true }, "node_modules/@schematics/angular": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.1.6.tgz", - "integrity": "sha512-TxFp6iHBqXcuyZIW84HA4z3XkAMz3wTw46K3GNhzyfhFTFD0YD+DtaR3MfQ+vcj3YUYu9j44zrB9nchzugR9Ew==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.1.7.tgz", + "integrity": "sha512-BB8yMGmYDZzSb8Nu+Ln0TKyeoS3++f9STCYw30NwM3IViHxJJYxu/zowzwSa9TjftIzdCpbOaPxGS0vU9UOUDQ==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.6", - "@angular-devkit/schematics": "19.1.6", + "@angular-devkit/core": "19.1.7", + "@angular-devkit/schematics": "19.1.7", "jsonc-parser": "3.3.1" }, "engines": { @@ -4143,9 +4160,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", + "version": "22.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", + "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", "dev": true, "license": "MIT", "dependencies": { @@ -10998,10 +11015,11 @@ } }, "node_modules/prettier": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz", - "integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/package.json b/package.json index d288a5d..40a57d3 100644 --- a/package.json +++ b/package.json @@ -17,16 +17,16 @@ }, "private": true, "dependencies": { - "@angular/animations": "^19.1.5", - "@angular/cdk": "~19.1.3", - "@angular/common": "^19.1.5", - "@angular/compiler": "^19.1.5", - "@angular/core": "^19.1.5", - "@angular/forms": "^19.1.5", - "@angular/material": "~19.1.3", - "@angular/platform-browser": "^19.1.5", - "@angular/platform-browser-dynamic": "^19.1.5", - "@angular/router": "^19.1.5", + "@angular/animations": "^19.1.6", + "@angular/cdk": "~19.1.4", + "@angular/common": "^19.1.6", + "@angular/compiler": "^19.1.6", + "@angular/core": "^19.1.6", + "@angular/forms": "^19.1.6", + "@angular/material": "~19.1.4", + "@angular/platform-browser": "^19.1.6", + "@angular/platform-browser-dynamic": "^19.1.6", + "@angular/router": "^19.1.6", "@fortawesome/angular-fontawesome": "^1.0.0", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", @@ -43,19 +43,19 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@angular/build": "^19.1.6", - "@angular/cli": "^19.1.6", - "@angular/compiler-cli": "^19.1.5", + "@angular/build": "^19.1.7", + "@angular/cli": "^19.1.7", + "@angular/compiler-cli": "^19.1.6", "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", "@release-it/conventional-changelog": "^10.0.0", - "@types/node": "^22.13.1", + "@types/node": "^22.13.4", "angular-eslint": "^19.1.0", "eslint": "^9.20.1", "eslint-plugin-import": "^2.31.0", "husky": "^9.1.7", "lint-staged": "^15.4.3", - "prettier": "^3.5.0", + "prettier": "^3.5.1", "prettier-plugin-tailwindcss": "^0.6.11", "release-it": "^18.1.2", "typescript": "~5.5.2", From cdb93f0a6be82ef3f79f364d19db75add55ed489 Mon Sep 17 00:00:00 2001 From: Nikos Anifantis Date: Mon, 17 Feb 2025 11:16:44 +0200 Subject: [PATCH 5/5] chore(app): standalone components by default --- src/app/app.component.ts | 1 - src/app/auth/login/login.component.ts | 1 - src/app/features/about/about.component.ts | 1 - src/app/features/home/home.component.ts | 1 - src/app/features/secured-feat/secured-feat.component.ts | 1 - src/app/shared/ui/avatar/avatar.component.ts | 1 - src/app/shared/ui/footer/footer.component.ts | 1 - src/app/shared/ui/header/header.component.ts | 1 - 8 files changed, 8 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2dd52aa..13f7da6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -11,7 +11,6 @@ import { HeaderComponent } from './shared/ui/header'; @Component({ selector: 'aa-root', - standalone: true, imports: [AsyncPipe, RouterOutlet, HeaderComponent, FooterComponent], template: `
diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts index 6728144..6098c66 100644 --- a/src/app/auth/login/login.component.ts +++ b/src/app/auth/login/login.component.ts @@ -11,7 +11,6 @@ import { AUTH_FACADE } from '../tokens'; @Component({ selector: 'aa-login', - standalone: true, imports: [ AsyncPipe, MatButtonModule, diff --git a/src/app/features/about/about.component.ts b/src/app/features/about/about.component.ts index 8ba698c..1b8d7e2 100644 --- a/src/app/features/about/about.component.ts +++ b/src/app/features/about/about.component.ts @@ -5,7 +5,6 @@ import { IconModule } from '../../shared/ui/icon'; @Component({ selector: 'aa-about', - standalone: true, imports: [MatButtonModule, IconModule], templateUrl: './about.component.html', }) diff --git a/src/app/features/home/home.component.ts b/src/app/features/home/home.component.ts index c6f8eaf..57e395c 100644 --- a/src/app/features/home/home.component.ts +++ b/src/app/features/home/home.component.ts @@ -9,7 +9,6 @@ import { Feature, features } from './features.data'; @Component({ selector: 'aa-home', - standalone: true, imports: [IconModule, MatButtonModule, MatCardModule, MatTooltipModule], templateUrl: './home.component.html', }) diff --git a/src/app/features/secured-feat/secured-feat.component.ts b/src/app/features/secured-feat/secured-feat.component.ts index e3a43cd..06c3649 100644 --- a/src/app/features/secured-feat/secured-feat.component.ts +++ b/src/app/features/secured-feat/secured-feat.component.ts @@ -9,7 +9,6 @@ import { USERS } from '../../core/fake-api'; import { GreetingUtil } from '../../shared/util'; @Component({ selector: 'aa-secured-feat', - standalone: true, imports: [AsyncPipe, MatCardModule, MatTableModule], templateUrl: './secured-feat.component.html', }) diff --git a/src/app/shared/ui/avatar/avatar.component.ts b/src/app/shared/ui/avatar/avatar.component.ts index 07aa8c1..7d73885 100644 --- a/src/app/shared/ui/avatar/avatar.component.ts +++ b/src/app/shared/ui/avatar/avatar.component.ts @@ -2,7 +2,6 @@ import { Component, Input } from '@angular/core'; @Component({ selector: 'aa-avatar', - standalone: true, template: ` {{ computedText }} `, diff --git a/src/app/shared/ui/footer/footer.component.ts b/src/app/shared/ui/footer/footer.component.ts index 82879ac..4d97e14 100644 --- a/src/app/shared/ui/footer/footer.component.ts +++ b/src/app/shared/ui/footer/footer.component.ts @@ -13,7 +13,6 @@ interface PersonalLink { @Component({ selector: 'aa-footer', - standalone: true, imports: [DatePipe, IconModule, MatButtonModule], templateUrl: './footer.component.html', }) diff --git a/src/app/shared/ui/header/header.component.ts b/src/app/shared/ui/header/header.component.ts index 538505e..eb1b1b9 100644 --- a/src/app/shared/ui/header/header.component.ts +++ b/src/app/shared/ui/header/header.component.ts @@ -17,7 +17,6 @@ interface MenuItem { @Component({ selector: 'aa-header', - standalone: true, imports: [ AvatarComponent, IconModule,