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: [](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: [](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/package-lock.json b/package-lock.json
index 3cded8c..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",
@@ -27,24 +27,26 @@
"@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"
},
"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",
@@ -80,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": {
@@ -95,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",
@@ -122,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",
@@ -257,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"
},
@@ -267,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",
@@ -317,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",
@@ -423,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"
},
@@ -439,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",
@@ -472,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"
},
@@ -482,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"
},
@@ -497,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": {
@@ -506,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",
@@ -529,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"
},
@@ -549,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"
},
@@ -559,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",
@@ -583,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"
},
@@ -593,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": {
@@ -604,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"
},
@@ -614,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"
},
@@ -631,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"
}
},
@@ -2887,6 +2905,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",
@@ -3943,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": {
@@ -4106,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": {
@@ -10961,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 fec462a..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",
@@ -36,24 +36,26 @@
"@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"
},
"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",
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 724f083..13f7da6 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -4,14 +4,13 @@ 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';
@Component({
selector: 'aa-root',
- standalone: true,
imports: [AsyncPipe, RouterOutlet, HeaderComponent, FooterComponent],
template: `
@@ -28,7 +27,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..c75a705 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -1,30 +1,18 @@
-import {
- provideHttpClient,
- withInterceptors,
- withInterceptorsFromDi,
-} from '@angular/common/http';
+import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
ApplicationConfig,
provideExperimentalZonelessChangeDetection,
} 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 } 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' })];
-}
+// ⚠️ FIXME: choose one store and remove any packages in real app ⚠️
+const storeType = StoreType.Ngxs;
export const appConfig: ApplicationConfig = {
providers: [
@@ -32,23 +20,20 @@ export const appConfig: ApplicationConfig = {
provideExperimentalZonelessChangeDetection(),
provideAnimationsAsync(),
- // Setup NgRx
- provideStore({ router: routerReducer }),
- provideRouterStore(),
- provideEffects(),
+ // Setup Store
+ provideSetupStore(storeType),
// Setup Interceptors
provideHttpClient(
- withInterceptorsFromDi(),
- // ⚠️ FIXME: remove it in real app ⚠️
- withInterceptors([fakeApiInterceptor])
+ withInterceptors([
+ authInterceptor,
+ // ⚠️ FIXME: remove it in real app ⚠️
+ fakeApiInterceptor,
+ ])
),
// 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..d84ce10
--- /dev/null
+++ b/src/app/app.store.ts
@@ -0,0 +1,71 @@
+/**
+ * ⚠️ 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,
+ 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',
+}
+
+/**
+ * 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: APP_NAME }) : [],
+ ],
+ ngxs: [
+ provideNgxsStore(
+ [],
+ withNgxsReduxDevtoolsPlugin({
+ name: APP_NAME,
+ disabled: !isDevToolsEnabled,
+ }),
+ withNgxsDevelopmentOptions({
+ warnOnUnhandledActions: true,
+ })
+ ),
+ ],
+ };
+
+ 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/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..7b7e4c3 100644
--- a/src/app/auth/index.ts
+++ b/src/app/auth/index.ts
@@ -1,20 +1,8 @@
-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 { authInterceptor } from './interceptors';
-export function provideAuthStore() {
- return [
- provideState(AUTH_FEATURE_KEY, authReducer),
- provideEffects(AuthEffects),
- authServiceInitProvider,
- authInterceptorProviders,
- ];
-}
+// Stores
+export { provideAuthStore as provideNgrxAuthStore } from './store/index.ngrx';
+export { provideAuthStore as provideNgxsAuthStore } from './store/index.ngxs';
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..6098c66 100644
--- a/src/app/auth/login/login.component.ts
+++ b/src/app/auth/login/login.component.ts
@@ -7,11 +7,10 @@ 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',
- standalone: true,
imports: [
AsyncPipe,
MatButtonModule,
@@ -24,7 +23,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/store/auth.models.ts b/src/app/auth/models/auth-state.model.ts
similarity index 71%
rename from src/app/auth/store/auth.models.ts
rename to src/app/auth/models/auth-state.model.ts
index 13199e6..66678af 100644
--- a/src/app/auth/store/auth.models.ts
+++ b/src/app/auth/models/auth-state.model.ts
@@ -1,3 +1,5 @@
+import { AuthUser } from './auth-user.model';
+
export enum TokenStatus {
PENDING = 'PENDING',
VALIDATING = 'VALIDATING',
@@ -5,7 +7,7 @@ export enum TokenStatus {
INVALID = 'INVALID',
}
-export interface AuthState {
+export interface AuthStateModel {
isLoggedIn: boolean;
user?: AuthUser;
accessTokenStatus: TokenStatus;
@@ -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/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..80b38c7
--- /dev/null
+++ b/src/app/auth/models/index.ts
@@ -0,0 +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.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/index.ngxs.ts b/src/app/auth/store/index.ngxs.ts
new file mode 100644
index 0000000..6ae5322
--- /dev/null
+++ b/src/app/auth/store/index.ngxs.ts
@@ -0,0 +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([
+ provideStates([AuthState]),
+ provideAuthInit(),
+ // Register Auth Facade
+ {
+ provide: AUTH_FACADE,
+ useClass: NgxsAuthFacade,
+ },
+ ]);
+}
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.reducer.ts b/src/app/auth/store/ngrx/auth.reducer.ts
similarity index 76%
rename from src/app/auth/store/auth.reducer.ts
rename to src/app/auth/store/ngrx/auth.reducer.ts
index 58a121b..9dda096 100644
--- a/src/app/auth/store/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/auth.selectors.ts b/src/app/auth/store/ngrx/auth.selectors.ts
similarity index 78%
rename from src/app/auth/store/auth.selectors.ts
rename to src/app/auth/store/ngrx/auth.selectors.ts
index 5f615ae..3f246d0 100644
--- a/src/app/auth/store/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
new file mode 100644
index 0000000..9d47229
--- /dev/null
+++ b/src/app/auth/store/ngrx/index.ts
@@ -0,0 +1,34 @@
+import { EnvironmentProviders, inject, provideAppInitializer } from '@angular/core';
+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 * 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/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/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/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/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/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.',
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 6c551d1..06c3649 100644
--- a/src/app/features/secured-feat/secured-feat.component.ts
+++ b/src/app/features/secured-feat/secured-feat.component.ts
@@ -4,17 +4,16 @@ 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({
selector: 'aa-secured-feat',
- standalone: true,
imports: [AsyncPipe, MatCardModule, MatTableModule],
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'];
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,