├── .editorconfig
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── 1-bug.md
│ ├── 2-feature.md
│ └── config.yml
├── pull_request_template.md
└── workflows
│ └── code-quality.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── angular.json
├── eslint.config.js
├── meta
├── app-demo.gif
├── auth-init-flow.png
└── auth-login-flow.png
├── package-lock.json
├── package.json
├── public
├── assets
│ ├── logo.svg
│ ├── ng-auth.png
│ └── security.svg
├── favicon.ico
└── netlify.toml
├── src
├── app
│ ├── app.component.scss
│ ├── app.component.ts
│ ├── app.config.ts
│ ├── app.routes.ts
│ ├── app.store.ts
│ ├── auth
│ │ ├── auth.routes.ts
│ │ ├── auth.service.ts
│ │ ├── guards
│ │ │ ├── auth.guard.ts
│ │ │ ├── index.ts
│ │ │ └── no-auth.guard.ts
│ │ ├── index.ts
│ │ ├── interceptors
│ │ │ ├── auth.interceptor.ts
│ │ │ └── index.ts
│ │ ├── login
│ │ │ ├── login.component.html
│ │ │ ├── login.component.scss
│ │ │ └── login.component.ts
│ │ ├── models
│ │ │ ├── auth-facade.model.ts
│ │ │ ├── auth-state.model.ts
│ │ │ ├── auth-user.model.ts
│ │ │ └── index.ts
│ │ ├── store
│ │ │ ├── index.ngrx.ts
│ │ │ ├── index.ngxs.ts
│ │ │ ├── ngrx
│ │ │ │ ├── auth.actions.ts
│ │ │ │ ├── auth.effects.ts
│ │ │ │ ├── auth.facade.ts
│ │ │ │ ├── auth.reducer.ts
│ │ │ │ ├── auth.selectors.ts
│ │ │ │ └── index.ts
│ │ │ └── ngxs
│ │ │ │ ├── auth.actions.ts
│ │ │ │ ├── auth.facade.ts
│ │ │ │ ├── auth.selectors.ts
│ │ │ │ ├── auth.state.ts
│ │ │ │ └── index.ts
│ │ └── tokens
│ │ │ ├── auth-facade.token.ts
│ │ │ └── index.ts
│ ├── core
│ │ ├── fake-api
│ │ │ ├── db.data.ts
│ │ │ ├── fake-api.interceptor.ts
│ │ │ ├── fake-api.ts
│ │ │ └── index.ts
│ │ └── services
│ │ │ ├── config.service.ts
│ │ │ ├── google-analytics.service.ts
│ │ │ ├── index.ts
│ │ │ ├── local-storage.service.ts
│ │ │ └── token-storage.service.ts
│ ├── features
│ │ ├── about
│ │ │ ├── about.component.html
│ │ │ ├── about.component.ts
│ │ │ └── index.ts
│ │ ├── home
│ │ │ ├── features.data.ts
│ │ │ ├── home.component.html
│ │ │ ├── home.component.ts
│ │ │ └── index.ts
│ │ └── secured-feat
│ │ │ ├── index.ts
│ │ │ ├── secured-feat.component.html
│ │ │ └── secured-feat.component.ts
│ └── shared
│ │ ├── pipes
│ │ ├── greeting.pipe.ts
│ │ └── index.ts
│ │ └── ui
│ │ ├── avatar
│ │ ├── avatar.component.scss
│ │ ├── avatar.component.ts
│ │ └── index.ts
│ │ ├── footer
│ │ ├── footer.component.html
│ │ ├── footer.component.ts
│ │ └── index.ts
│ │ ├── header
│ │ ├── header.component.html
│ │ ├── header.component.scss
│ │ ├── header.component.ts
│ │ └── index.ts
│ │ └── icon
│ │ ├── icon.module.ts
│ │ └── index.ts
├── environments
│ ├── environment.development.ts
│ └── environment.ts
├── index.html
├── main.ts
├── styles.scss
└── theme
│ ├── _components.scss
│ ├── _material.scss
│ └── index.scss
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.eslint.json
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These owners will be the default owners for everything in the repo.
2 | * @nikosanif
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1-bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: '🐞 Bug Report'
3 | about: Report a bug
4 | labels: 'type: bug'
5 | ---
6 |
7 |
8 |
9 | ## Current Behavior
10 |
11 |
12 |
13 | ## Expected Behavior
14 |
15 |
16 |
17 |
18 | ## Steps to Reproduce
19 |
20 |
21 |
22 | ### Failure Logs
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2-feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F680 Feature Request"
3 | about: Suggest a new feature.
4 | labels: 'type: feature'
5 | ---
6 |
7 |
8 |
9 | ## Description
10 |
11 |
12 |
13 | ## Motivation
14 |
15 |
16 |
17 | ## Suggested Implementation
18 |
19 |
20 |
21 | ## Alternate Implementations
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Current Behaviour
2 |
3 | # Expected Behaviour
4 |
5 | # Related Issues
6 |
7 | Fixes #
8 |
--------------------------------------------------------------------------------
/.github/workflows/code-quality.yml:
--------------------------------------------------------------------------------
1 | name: Code Quality Check
2 |
3 | on:
4 | # Trigger the workflow on push or pull request,
5 | # but only for the main & develop branch
6 | push:
7 | branches:
8 | - main
9 | - develop
10 | pull_request:
11 | branches:
12 | - main
13 | - develop
14 |
15 | jobs:
16 | linting:
17 | name: Linting Check
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout current repository
21 | uses: actions/checkout@v4
22 |
23 | - name: Set up Node.js
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: '22.x'
27 |
28 | - name: Cache node modules
29 | uses: actions/cache@v4
30 | with:
31 | path: '**/node_modules'
32 | key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
33 | restore-keys: |
34 | ${{ runner.os }}-node-modules-
35 | ${{ runner.os }}-
36 |
37 | - name: Install dependencies
38 | run: npm ci
39 |
40 | - name: Run linting script
41 | run: npm run lint
42 |
43 | format:
44 | name: Format Check
45 | runs-on: ubuntu-latest
46 | steps:
47 | - name: Checkout current repository
48 | uses: actions/checkout@v4
49 |
50 | - name: Set up Node.js
51 | uses: actions/setup-node@v4
52 | with:
53 | node-version: '22.x'
54 |
55 | - name: Cache node modules
56 | uses: actions/cache@v4
57 | with:
58 | path: '**/node_modules'
59 | key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
60 | restore-keys: |
61 | ${{ runner.os }}-node-modules-
62 | ${{ runner.os }}-
63 |
64 | - name: Install dependencies
65 | run: npm ci
66 |
67 | - name: Run format script
68 | run: npm run format:check
69 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
44 | # Plugins
45 | .eslintcache
46 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx commitlint --edit $1
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | lockfileVersion=3
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | # Default
4 | /dist
5 | /coverage
6 | /tmp
7 |
8 | # Plugins
9 | .angular
10 | .eslintcache
11 |
12 | # App
13 | package-lock.json
14 | package.json
15 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "printWidth": 90,
4 | "semi": true,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "trailingComma": "es5",
8 | "useTabs": false,
9 | "htmlWhitespaceSensitivity": "ignore",
10 | "plugins": ["prettier-plugin-tailwindcss"]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "angular.ng-template",
4 | "esbenp.prettier-vscode",
5 | "PKief.material-icon-theme",
6 | "dbaeumer.vscode-eslint",
7 | "vivaxy.vscode-conventional-commits"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "editor.rulers": [90],
4 | "editor.formatOnSave": true,
5 |
6 | "typescript.preferences.importModuleSpecifier": "relative",
7 | "typescript.preferences.quoteStyle": "single",
8 |
9 | "workbench.iconTheme": "material-icon-theme",
10 |
11 | "workbench.colorCustomizations": {
12 | "titleBar.activeBackground": "#18162d",
13 | "titleBar.inactiveBackground": "#18162d99",
14 |
15 | "activityBar.background": "#18162d",
16 | "activityBar.activeBackground": "#28254b",
17 | "activityBar.activeBorder": "#01b698",
18 | "activityBar.foreground": "#01b698",
19 | "activityBar.inactiveForeground": "#00a489b7",
20 |
21 | "activityBarBadge.background": "#f7ba31",
22 | "activityBarBadge.foreground": "#18162d",
23 |
24 | "statusBar.background": "#01b698",
25 | "statusBar.foreground": "#18162d",
26 |
27 | "statusBarItem.hoverBackground": "#00a489b7",
28 | "statusBarItem.activeBackground": "#01725fb7"
29 | },
30 |
31 | "conventionalCommits.scopes": [
32 | "workspace",
33 | "app",
34 | "shared",
35 | "core",
36 | "ui",
37 | "auth",
38 | "utils",
39 | "models"
40 | ],
41 |
42 | "explorer.fileNesting.enabled": true,
43 | "explorer.fileNesting.expand": false,
44 | "explorer.fileNesting.patterns": {
45 | "readme.*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 2.2.0 (2025-02-17)
4 |
5 | - feat(store): support NGXS state management (#10) ([b77e95b](https://github.com/nikosanif/angular-authentication/commit/b77e95b)), closes [#10](https://github.com/nikosanif/angular-authentication/issues/10)
6 | - chore: update to Node.js v22 ([69a5a11](https://github.com/nikosanif/angular-authentication/commit/69a5a11))
7 | - build: update to Angular 19 ([dab32da](https://github.com/nikosanif/angular-authentication/commit/dab32da))
8 | - docs: update readme file ([374e60a](https://github.com/nikosanif/angular-authentication/commit/374e60a))
9 |
10 | ## 2.1.0 (2024-10-31)
11 |
12 | - feat(app): towards Angular Zoneless (#9) ([0525298](https://github.com/nikosanif/angular-authentication/commit/0525298)), closes [#9](https://github.com/nikosanif/angular-authentication/issues/9)
13 |
14 | ## [2.0.0](https://github.com/nikosanif/angular-authentication/compare/v1.0.1...v2.0.0) (2024-06-18)
15 |
16 | ### Features
17 |
18 | - add google analytics ([6857211](https://github.com/nikosanif/angular-authentication/commit/6857211a028dd9c6b522ef9451bc0a0fc832ab78))
19 | - add open graph and twitter meta tags ([da4f9dd](https://github.com/nikosanif/angular-authentication/commit/da4f9ddeba4ff791317e8c2d9fefbf0a64775ce0))
20 | - **app:** update app to Angular 18 ([#8](https://github.com/nikosanif/angular-authentication/issues/8)) ([0485383](https://github.com/nikosanif/angular-authentication/commit/0485383179e662bab2ef0438bf2feb6310675c46))
21 | - **icons:** create module for icons ([74664ea](https://github.com/nikosanif/angular-authentication/commit/74664eaea4c1e585f663497e0e815f70855cf513))
22 |
23 | ### Build System
24 |
25 | - install prettier-plugin for tailwindcss ([bc03223](https://github.com/nikosanif/angular-authentication/commit/bc03223524133e196e687c4febd4db5611c27cf5))
26 | - setup unit testing tools ([#2](https://github.com/nikosanif/angular-authentication/issues/2)) ([61695a9](https://github.com/nikosanif/angular-authentication/commit/61695a902f5a5cd9aa61b1d012b7fbe67bac6d13)), closes [#1](https://github.com/nikosanif/angular-authentication/issues/1)
27 |
28 | All notable changes to this project will be documented in this file.
29 |
30 | ### [1.0.1](https://github.com/nikosanif/angular-authentication/compare/v1.0.0...v1.0.1) (2021-12-23)
31 |
32 | ### Build System
33 |
34 | - add `analyze` script ([1288bca](https://github.com/nikosanif/angular-authentication/commit/1288bca41cab1b4c615ef429bf72f3997271bf8b))
35 | - ng update to v13 ([84ab6f9](https://github.com/nikosanif/angular-authentication/commit/84ab6f9da75f61a63af6a51a795868ae7c1b67cb))
36 | - update angular and packages ([767203b](https://github.com/nikosanif/angular-authentication/commit/767203bba5e8ff462e84c9e1f3a302c42fd2e4d3))
37 | - update tailwindcss to v3 ([77e6a4e](https://github.com/nikosanif/angular-authentication/commit/77e6a4e0718e1309568a59c81a97800158e87de8))
38 | - upgrade Angular to v13 and packages ([3993e46](https://github.com/nikosanif/angular-authentication/commit/3993e46eaab44976193e8be95b9ac6c9ab888461))
39 |
40 | ## 1.0.0 (2021-10-20)
41 |
42 | ### Features
43 |
44 | - add app's features at home ([10044b5](https://github.com/nikosanif/angular-authentication/commit/10044b5c491f8429e8d136630f97ff414cce056f))
45 | - add content at secured-feat component ([d76f4f0](https://github.com/nikosanif/angular-authentication/commit/d76f4f0ffd4dfb9ca9afd546a8b8f8b20c21c85a))
46 | - add hint at login form ([1eb50ed](https://github.com/nikosanif/angular-authentication/commit/1eb50edb1e8c46e937f566bedd3a1f72f59f85bf))
47 | - add new module `about` ([ab26a8f](https://github.com/nikosanif/angular-authentication/commit/ab26a8f5f1ab37a03dc3d34dee7a877997445f78))
48 | - add settings at environments ([2c7e308](https://github.com/nikosanif/angular-authentication/commit/2c7e3084d2533a929815febb4bcf63f914b4acb6))
49 | - add users account list ([150e592](https://github.com/nikosanif/angular-authentication/commit/150e5929feb67f84d6dd09ecf771cc414941256f))
50 | - **app:** add footer at main layout ([df7fa55](https://github.com/nikosanif/angular-authentication/commit/df7fa55a51fc2b53911961c499c463cc1ca4977d))
51 | - **app:** create app main header ([5243f65](https://github.com/nikosanif/angular-authentication/commit/5243f6503b6f8fa9393d06317b2f7023d907a90c))
52 | - **app:** create custom `card` component style ([f4e496f](https://github.com/nikosanif/angular-authentication/commit/f4e496f238dae06332b3093482afd73c22d4b6dd))
53 | - **app:** create typography style and fix responsive layout ([0b7c216](https://github.com/nikosanif/angular-authentication/commit/0b7c2162fcc40819ca97a3a854b6798f80e91b9f))
54 | - **app:** show current version at footer ([a0fa7f9](https://github.com/nikosanif/angular-authentication/commit/a0fa7f93d5a162904ac27b9467251a6571d6834d))
55 | - **auth:** create auth module ([6a6c6d8](https://github.com/nikosanif/angular-authentication/commit/6a6c6d8754a4d001760ba55eb30d6f98569f00d6))
56 | - **auth:** create dummy secured-feat module ([2111867](https://github.com/nikosanif/angular-authentication/commit/211186729422ece612d36015b943c8d96549f7ba))
57 | - **auth:** design login form ([54f14c0](https://github.com/nikosanif/angular-authentication/commit/54f14c02229a0c82a29287af2d66209cb4029f8c))
58 | - **auth:** init and setup store ([5f13f0f](https://github.com/nikosanif/angular-authentication/commit/5f13f0f4d1a7d9d30b41a8828c8caa30fe5cabc3))
59 | - **core:** add main services for core ([caac0b4](https://github.com/nikosanif/angular-authentication/commit/caac0b476c6aad58097952500ad95ba4399b7152))
60 | - **core:** create core module and import @ngrx/\* ([7bc4289](https://github.com/nikosanif/angular-authentication/commit/7bc4289a685d08bdb06da86c0073b9149fa4f9e8))
61 | - enrich app's features list ([64f2f9c](https://github.com/nikosanif/angular-authentication/commit/64f2f9c3eb02235aa7af97e839871131aa015bb6))
62 | - finalize auth in fake-api ([3cca23e](https://github.com/nikosanif/angular-authentication/commit/3cca23eb144b2ad5502b418ebe98266ed0d7e1a7))
63 | - **header:** add icons at menu items ([2f03887](https://github.com/nikosanif/angular-authentication/commit/2f03887213313a3a58b23f62b05f28ebd92699ff))
64 | - **header:** add repo's link at github ([5ab2a9b](https://github.com/nikosanif/angular-authentication/commit/5ab2a9b8873bb1ab6cbefefbfaac2e49a4ac757e))
65 | - **home:** create new module for home ([cc18510](https://github.com/nikosanif/angular-authentication/commit/cc18510258ddbc5397fb9f27a8a4ae0a2a66aac9))
66 | - implement fake api (wip) ([23c5b08](https://github.com/nikosanif/angular-authentication/commit/23c5b0801373893ef2ac487ecde7e2e8b2644b86))
67 | - setup theme and main header ([4350702](https://github.com/nikosanif/angular-authentication/commit/435070220f50138e858beaca71ec68d085868b52))
68 | - **shared:** add greeting utils ([f447aff](https://github.com/nikosanif/angular-authentication/commit/f447aff263d1e650c8e67da9349f021d64f323a8))
69 | - **ui:** add content at about component ([5a38442](https://github.com/nikosanif/angular-authentication/commit/5a384426cb658d73f37cdedfe7926a0fb7f4682b))
70 | - **ui:** create footer module ([fddd150](https://github.com/nikosanif/angular-authentication/commit/fddd150c934b894729494a85f6c377d85d915b8e))
71 | - **ui:** create new module for header ([d1b9321](https://github.com/nikosanif/angular-authentication/commit/d1b9321bc58f8f488de593a71f03ddc738ba1bde))
72 | - **ui:** design footer component ([f44e509](https://github.com/nikosanif/angular-authentication/commit/f44e509db5811c506451c876dfb21f1e6f2cf44d))
73 |
74 | ### Bug Fixes
75 |
76 | - add handler for auth logout fake request ([d5d2610](https://github.com/nikosanif/angular-authentication/commit/d5d26108258194f7b9b603c7d6d457d258dbc810))
77 | - change footer creator text ([6a025ac](https://github.com/nikosanif/angular-authentication/commit/6a025ac641b296c40658f8ef2271581e849cebda))
78 | - remove greeting from login ([29f3011](https://github.com/nikosanif/angular-authentication/commit/29f3011357e862685c3636423eb553d601f78a77))
79 | - remove list from support section ([61d446b](https://github.com/nikosanif/angular-authentication/commit/61d446b6fdaf88c33d9da600dc96a3c828b4e2eb))
80 | - style changes at header ([6829952](https://github.com/nikosanif/angular-authentication/commit/6829952d31ff99328bb653dc62a2f6b4bc8483ec))
81 | - **ui:** make footer responsive ([3a67ded](https://github.com/nikosanif/angular-authentication/commit/3a67ded5200021affa52be6c0b7e74f053dbbf21))
82 | - use `exhaustMap` instead of `switchMap` at effects ([41ff579](https://github.com/nikosanif/angular-authentication/commit/41ff57958bafe4c692223ca55b0aafba0127edfc))
83 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Angular Authentication
2 |
3 | I would love for you to contribute to Angular Authentication!
4 | As a contributor, here are the guidelines we would like you to follow:
5 |
6 | - [Bugs and Feature Requests](#issue)
7 | - [Submission Guidelines](#submit)
8 | - [Coding Rules](#rules)
9 | - [Commit Message Guidelines](#commit)
10 |
11 | ## Found a Bug or Missing a Feature?
12 |
13 | If you find a bug in the source code or want to _request_ a new feature, you can help by [submitting an issue](#submit-issue) to this [GitHub Repository](https://github.com/nikosanif/angular-authentication).
14 | Even better, you can [submit a PR](#submit-pr) with the fix or the new feature description.
15 |
16 | ## Submission Guidelines
17 |
18 | ### Submitting an Issue
19 |
20 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
21 |
22 | You can submit a new issue [here](https://github.com/nikosanif/angular-authentication/issues/new/choose).
23 |
24 | ### Submitting a Pull Request (PR)
25 |
26 | Before you submit your Pull Request (PR) consider the following guidelines:
27 |
28 | 1. Search [GitHub](https://github.com/nikosanif/angular-authentication/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate existing efforts.
29 |
30 | 2. Create a new branch with a meaningful name or issue id name.
31 |
32 | 3. In your branch, make your changes in a new git branch:
33 |
34 | ```shell
35 | git checkout -b my-fix-branch master
36 | ```
37 |
38 | 4. Create your patch.
39 |
40 | 5. Follow [Coding Rules](#rules).
41 |
42 | 6. Commit your changes _preferably_ using a descriptive commit message that follows [commit message conventions](#commit).
43 | Adherence to these conventions is not necessary but you will save us time because release notes are automatically generated from these messages.
44 |
45 | ```shell
46 | git commit --all
47 | ```
48 |
49 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
50 |
51 | 7. Push your branch to GitHub:
52 |
53 | ```shell
54 | git push origin my-fix-branch
55 | ```
56 |
57 | 8. Make sure that you have merged any potential changes and resolve conflicts.
58 |
59 | 9. In GitHub, send a pull request to `main`.
60 |
61 | ## Coding Rules
62 |
63 | To ensure consistency throughout the source code, keep these rules in mind when you are ready to submit a PR:
64 |
65 | - Run `npm run format:write` for code style enforcement.
66 | - Run `npm run lint` for linting.
67 | - Follow SOLID & DRY principles.
68 | - Keep documentation updated.
69 |
70 | ## Commit Message Format
71 |
72 | Our commit messages follow the [conventional commits specification](https://www.conventionalcommits.org/).
73 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Nikos Anifantis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular Authentication
2 |
3 | An Angular application that demonstrates best practices for user authentication & authorization flows.
4 |
5 | By [@nikosanif](https://x.com/nikosanif)
6 |
7 | [](https://github.com/nikosanif/angular-authentication/blob/main/LICENSE)
8 | [](https://angular-authentication.netlify.app)
9 | [](https://github.com/prettier/prettier)
10 | [](https://x.com/nikosanif)
11 |
12 | ## Table of Contents
13 |
14 | - [Live Demo](#live-demo)
15 | - [Getting Started](#getting-started)
16 | - [Features](#features)
17 | - [Tech Stack](#tech-stack)
18 | - [High-level Design](#high-level-design)
19 | - [Contributing](#contributing)
20 | - [Support](#support)
21 | - [License](#license)
22 |
23 | ## Live Demo
24 |
25 | Live application: [angular-authentication.netlify.app](https://angular-authentication.netlify.app/)
26 |
27 | 
28 |
29 | ## Getting Started
30 |
31 | ### Prerequisites
32 |
33 | - [Node.js](https://nodejs.org/en/)
34 | - [Angular CLI](https://angular.io/cli)
35 |
36 | ### Setup & Local Development
37 |
38 | - Clone this repository: `git clone git@github.com:nikosanif/angular-authentication.git`
39 | - `cd angular-authentication`
40 | - Install dependencies: `npm install`
41 | - Serve the Angular app: `npm start`
42 | - Open your browser at: `http://localhost:4200`
43 |
44 | ### Use it as a Template
45 |
46 | 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.
47 |
48 | If you want to use this repository as a template for your project, you can follow these steps:
49 |
50 | - Clone this repository
51 | - Remove fake API:
52 | - Delete `src/app/core/fake-api` folder
53 | - Remove all references from the `fake-api` folder
54 | - Remove the `fakeApiInterceptor` from `app.config.ts`
55 | - Choose the state management library you want to use:
56 | - NgRx: Remove `src/app/auth/store/ngxs` folder and the `index.ngxs.ts` file
57 | - NGXS: Remove `src/app/auth/store/ngrx` folder and the `index.ngrx.ts` file
58 | - Rename the `index.XXX.ts` file to `index.ts` in the `src/app/auth/store` folder
59 | - Update the `app.store.ts` file to import the correct store module
60 | - Remove all unused packages from `package.json`
61 | - 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.
62 |
63 | ### Useful Commands
64 |
65 | - `npm start` - starts a dev server of Angular app
66 | - `npm run build:prod` - builds full prod build
67 | - `npm run lint` - linting source code of this project
68 | - `npm run format:check` - runs prettier to check for formatting errors
69 | - `npm run format:write` - runs prettier to format whole code base
70 | - `npm run release` - runs `release-it` to create new release
71 |
72 | ## Features
73 |
74 | ### Authentication Flows
75 |
76 | 
77 | 
78 |
79 | ### Other Features
80 |
81 | - Zoneless Angular application
82 | - Standalone Angular components
83 | - Angular Material UI components
84 | - Lazy loading of Angular components
85 | - API requests with `@ngrx/effects` or `@ngxs/store` (you can choose at `src/app/app.config.ts`)
86 | - Responsive design
87 | - Custom In-memory Web API using interceptors
88 |
89 | ## Tech Stack
90 |
91 | - [Angular](https://angular.io/)
92 | - 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.
93 | - [NgRX](https://ngrx.io/) - @ngrx/{store,effects,component}
94 | - [NGXS](https://www.ngxs.io/) - @ngxs/store
95 | - [Angular Material UI](https://material.angular.io/)
96 | - [Tailwind CSS](https://tailwindcss.com/)
97 | - Other dev tools
98 | - ESLint
99 | - Prettier
100 | - Husky
101 | - release-it
102 |
103 | ## High-level Design
104 |
105 | Below is the high-level structure of the application.
106 |
107 | ```sh
108 | ./src
109 | ├── app
110 | │ ├── app.component.scss
111 | │ ├── app.component.ts
112 | │ ├── app.config.ts
113 | │ ├── app.routes.ts
114 | │ ├── app.store.ts # configure store based on NgRx or NGXS
115 | │ │
116 | │ ├── auth # includes authentication logic
117 | │ │ ├── auth.routes.ts
118 | │ │ ├── auth.service.ts
119 | │ │ ├── index.ts
120 | │ │ ├── guards
121 | │ │ ├── interceptors
122 | │ │ ├── login
123 | │ │ ├── models
124 | │ │ ├── tokens
125 | │ │ └── store # Choose one of the following
126 | │ │ ├── ngrx # store based on NgRx
127 | │ │ └── ngxs # store based on NGXS
128 | │ │
129 | │ ├── core # includes core utilities
130 | │ │ ├── fake-api
131 | │ │ └── services
132 | │ │
133 | │ ├── features # all features of application
134 | │ │ ├── about
135 | │ │ ├── home
136 | │ │ └── secured-feat
137 | │ │
138 | │ └── shared
139 | │ ├── ui # UI components
140 | │ │ ├── avatar
141 | │ │ ├── footer
142 | │ │ ├── header
143 | │ │ └── icon
144 | │ │
145 | │ └── util # utility functions
146 | │
147 | ├── environments # environment configurations
148 | │
149 | ├── index.html
150 | ├── main.ts
151 | ├── styles.scss
152 | │
153 | └── theme # global theme styles
154 | ├── _components.scss
155 | ├── _material.scss
156 | └── index.scss
157 | ```
158 |
159 | ## Contributing
160 |
161 | Who is for this? I would love for you to contribute to Angular Authentication! Before you start, please read the [Contributor Guide](https://github.com/nikosanif/angular-authentication/blob/main/CONTRIBUTING.md).
162 |
163 | If you have found any bug in the source code or want to _request_ a new feature, you can help by [submitting an issue](https://github.com/nikosanif/angular-authentication/issues/new/choose) at GitHub. Even better, you can fork this repository and [submit a PR](https://github.com/nikosanif/angular-authentication/compare) with the fix or the new feature description.
164 |
165 | ## Support
166 |
167 | - Star this repository 👆⭐️
168 | - 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)
169 |
170 | ### Author: Nikos Anifantis ✍️
171 |
172 | - Fullstack Software Engineer - I’m currently working on Angular & Node.js application development.
173 | - I write stuff at [dev.to/nikosanif](https://dev.to/nikosanif) and [nikosanif.medium.com](https://nikosanif.medium.com/)
174 | - How to reach me: [](https://x.com/nikosanif) or [](https://www.linkedin.com/in/nikosanifantis/)
175 |
176 | ## License
177 |
178 | Feel free to use this repository, but **please star and put a reference to this repository.** :pray: :heart:
179 |
180 | [MIT](https://opensource.org/licenses/MIT)
181 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-authentication": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "aa",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular/build:application",
19 | "options": {
20 | "outputPath": "dist/angular-authentication",
21 | "index": "src/index.html",
22 | "browser": "src/main.ts",
23 | "polyfills": [],
24 | "tsConfig": "tsconfig.app.json",
25 | "inlineStyleLanguage": "scss",
26 | "assets": [
27 | {
28 | "glob": "**/*",
29 | "input": "public"
30 | }
31 | ],
32 | "styles": ["src/styles.scss"],
33 | "scripts": []
34 | },
35 | "configurations": {
36 | "production": {
37 | "budgets": [
38 | {
39 | "type": "initial",
40 | "maximumWarning": "500kB",
41 | "maximumError": "1MB"
42 | },
43 | {
44 | "type": "anyComponentStyle",
45 | "maximumWarning": "2kB",
46 | "maximumError": "4kB"
47 | }
48 | ],
49 | "outputHashing": "all"
50 | },
51 | "development": {
52 | "optimization": false,
53 | "extractLicenses": false,
54 | "sourceMap": true,
55 | "fileReplacements": [
56 | {
57 | "replace": "src/environments/environment.ts",
58 | "with": "src/environments/environment.development.ts"
59 | }
60 | ]
61 | }
62 | },
63 | "defaultConfiguration": "production"
64 | },
65 | "serve": {
66 | "builder": "@angular/build:dev-server",
67 | "configurations": {
68 | "production": {
69 | "buildTarget": "angular-authentication:build:production"
70 | },
71 | "development": {
72 | "buildTarget": "angular-authentication:build:development"
73 | }
74 | },
75 | "defaultConfiguration": "development"
76 | },
77 | "extract-i18n": {
78 | "builder": "@angular/build:extract-i18n"
79 | },
80 | "lint": {
81 | "builder": "@angular-eslint/builder:lint",
82 | "options": {
83 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const eslint = require('@eslint/js');
3 | const tseslint = require('typescript-eslint');
4 | const angular = require('angular-eslint');
5 | const importPlugin = require('eslint-plugin-import');
6 |
7 | module.exports = tseslint.config(
8 | {
9 | files: ['**/*.ts'],
10 | extends: [
11 | eslint.configs.recommended,
12 | ...tseslint.configs.recommended,
13 | ...tseslint.configs.stylistic,
14 | ...angular.configs.tsRecommended,
15 | ],
16 | processor: angular.processInlineTemplates,
17 | plugins: {
18 | import: importPlugin,
19 | },
20 | rules: {
21 | '@angular-eslint/directive-selector': [
22 | 'error',
23 | {
24 | type: 'attribute',
25 | prefix: 'aa',
26 | style: 'camelCase',
27 | },
28 | ],
29 | '@angular-eslint/component-selector': [
30 | 'error',
31 | {
32 | type: 'element',
33 | prefix: 'aa',
34 | style: 'kebab-case',
35 | },
36 | ],
37 |
38 | // Import rules
39 | 'import/order': [
40 | 'error',
41 | {
42 | alphabetize: {
43 | order: 'asc',
44 | },
45 | pathGroupsExcludedImportTypes: ['internal'],
46 | groups: [['builtin', 'external'], 'internal', 'type', 'parent'],
47 | 'newlines-between': 'always',
48 | },
49 | ],
50 | 'import/first': ['error'],
51 | 'import/no-duplicates': ['error'],
52 |
53 | // Member ordering
54 | '@typescript-eslint/member-ordering': [
55 | 'error',
56 | {
57 | default: [
58 | 'signature',
59 | 'public-static-field',
60 | 'protected-static-field',
61 | 'private-static-field',
62 | 'public-abstract-field',
63 | 'protected-abstract-field',
64 | 'private-decorated-field',
65 | 'private-instance-field',
66 | 'protected-decorated-field',
67 | 'protected-instance-field',
68 | 'public-decorated-field',
69 | 'public-instance-field',
70 | 'public-constructor',
71 | 'protected-constructor',
72 | 'private-constructor',
73 | 'public-static-method',
74 | 'protected-static-method',
75 | 'private-static-method',
76 | 'public-abstract-get',
77 | 'public-abstract-set',
78 | 'protected-abstract-get',
79 | 'protected-abstract-set',
80 | 'public-abstract-method',
81 | 'protected-abstract-method',
82 | 'public-decorated-method',
83 | 'public-instance-method',
84 | 'protected-decorated-method',
85 | 'protected-instance-method',
86 | 'private-decorated-method',
87 | 'private-instance-method',
88 | ],
89 | },
90 | ],
91 |
92 | '@typescript-eslint/explicit-member-accessibility': [
93 | 'error',
94 | {
95 | accessibility: 'no-public',
96 | },
97 | ],
98 | },
99 | },
100 | {
101 | files: ['**/*.html'],
102 | extends: [
103 | ...angular.configs.templateRecommended,
104 | ...angular.configs.templateAccessibility,
105 | ],
106 | rules: {
107 | '@angular-eslint/template/prefer-self-closing-tags': ['error'],
108 | },
109 | }
110 | );
111 |
--------------------------------------------------------------------------------
/meta/app-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikosanif/angular-authentication/95a920cfec1bef05d951086efc8a0e17c36c672b/meta/app-demo.gif
--------------------------------------------------------------------------------
/meta/auth-init-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikosanif/angular-authentication/95a920cfec1bef05d951086efc8a0e17c36c672b/meta/auth-init-flow.png
--------------------------------------------------------------------------------
/meta/auth-login-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikosanif/angular-authentication/95a920cfec1bef05d951086efc8a0e17c36c672b/meta/auth-login-flow.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-authentication",
3 | "version": "2.2.0",
4 | "engines": {
5 | "node": "^22"
6 | },
7 | "scripts": {
8 | "build": "ng build",
9 | "build:prod": "ng build --configuration production",
10 | "build:watch": "ng build --watch --configuration development",
11 | "format:check": "prettier --check .",
12 | "format:write": "prettier --write .",
13 | "lint": "ng lint",
14 | "prepare": "husky",
15 | "release": "release-it",
16 | "start": "ng serve"
17 | },
18 | "private": true,
19 | "dependencies": {
20 | "@angular/animations": "^19.2.8",
21 | "@angular/cdk": "~19.2.11",
22 | "@angular/common": "^19.2.8",
23 | "@angular/compiler": "^19.2.8",
24 | "@angular/core": "^19.2.8",
25 | "@angular/forms": "^19.2.8",
26 | "@angular/material": "~19.2.11",
27 | "@angular/platform-browser": "^19.2.8",
28 | "@angular/platform-browser-dynamic": "^19.2.8",
29 | "@angular/router": "^19.2.8",
30 | "@fortawesome/angular-fontawesome": "^1.0.0",
31 | "@fortawesome/fontawesome-svg-core": "^6.7.2",
32 | "@fortawesome/free-brands-svg-icons": "^6.7.2",
33 | "@fortawesome/free-solid-svg-icons": "^6.7.2",
34 | "@ngrx/component": "^19.1.0",
35 | "@ngrx/effects": "^19.1.0",
36 | "@ngrx/router-store": "^19.1.0",
37 | "@ngrx/store": "^19.1.0",
38 | "@ngrx/store-devtools": "^19.1.0",
39 | "@ngxs/devtools-plugin": "19.0.0",
40 | "@ngxs/store": "^19.0.0",
41 | "rxjs": "^7.8.2",
42 | "tailwindcss": "^3.4.14",
43 | "tslib": "^2.8.1"
44 | },
45 | "devDependencies": {
46 | "@angular/build": "^19.2.9",
47 | "@angular/cli": "^19.2.9",
48 | "@angular/compiler-cli": "^19.2.8",
49 | "@commitlint/cli": "^19.8.0",
50 | "@commitlint/config-conventional": "^19.8.0",
51 | "@release-it/conventional-changelog": "^10.0.1",
52 | "@types/node": "^22.15.2",
53 | "angular-eslint": "^19.3.0",
54 | "eslint": "^9.25.1",
55 | "eslint-plugin-import": "^2.31.0",
56 | "husky": "^9.1.7",
57 | "lint-staged": "^15.5.1",
58 | "prettier": "^3.5.3",
59 | "prettier-plugin-tailwindcss": "^0.6.11",
60 | "release-it": "^18.1.2",
61 | "typescript": "~5.8.3",
62 | "typescript-eslint": "^8.31.0"
63 | },
64 | "lint-staged": {
65 | "*.{js,ts,html,css}": "eslint --cache",
66 | "*": "prettier --list-different --ignore-unknown"
67 | },
68 | "commitlint": {
69 | "extends": [
70 | "@commitlint/config-conventional"
71 | ]
72 | },
73 | "release-it": {
74 | "git": {
75 | "commitMessage": "chore(release): ${version}",
76 | "requireBranch": [
77 | "main"
78 | ],
79 | "tag": true
80 | },
81 | "npm": {
82 | "publish": false
83 | },
84 | "github": {
85 | "release": true,
86 | "releaseName": "v${version}"
87 | },
88 | "plugins": {
89 | "@release-it/conventional-changelog": {
90 | "infile": "CHANGELOG.md",
91 | "header": "# Changelog",
92 | "ignoreRecommendedBump": true,
93 | "preset": {
94 | "name": "conventionalcommits",
95 | "types": [
96 | {
97 | "type": "feat",
98 | "section": "Features"
99 | },
100 | {
101 | "type": "fix",
102 | "section": "Bug Fixes"
103 | },
104 | {
105 | "type": "build",
106 | "section": "Build System"
107 | }
108 | ]
109 | }
110 | }
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/public/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/public/assets/ng-auth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikosanif/angular-authentication/95a920cfec1bef05d951086efc8a0e17c36c672b/public/assets/ng-auth.png
--------------------------------------------------------------------------------
/public/assets/security.svg:
--------------------------------------------------------------------------------
1 | security
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nikosanif/angular-authentication/95a920cfec1bef05d951086efc8a0e17c36c672b/public/favicon.ico
--------------------------------------------------------------------------------
/public/netlify.toml:
--------------------------------------------------------------------------------
1 | [[redirects]]
2 | from = "/*"
3 | to = "/index.html"
4 | status = 200
5 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | height: 100%;
3 | width: 100%;
4 |
5 | aa-header {
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | right: 0;
10 | z-index: 1;
11 | }
12 |
13 | .main-content {
14 | height: 100%;
15 | width: 100%;
16 | display: flex;
17 | flex-direction: column;
18 |
19 | main {
20 | background-color: #f4f3f7;
21 | position: relative;
22 | z-index: 0;
23 | margin-top: 4rem;
24 | flex: 1 0 auto;
25 | overflow: hidden;
26 | padding-bottom: 10px;
27 | padding-left: 1.5rem !important;
28 | padding-right: 1.5rem !important;
29 | }
30 |
31 | aa-footer {
32 | flex: 0 0 auto;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe } from '@angular/common';
2 | import { Component, DestroyRef, OnInit, inject } from '@angular/core';
3 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4 | import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
5 | import { filter } from 'rxjs';
6 |
7 | import { AUTH_FACADE } from './auth';
8 | import { ConfigService, GoogleAnalyticsService } from './core/services';
9 | import { FooterComponent } from './shared/ui/footer';
10 | import { HeaderComponent } from './shared/ui/header';
11 |
12 | @Component({
13 | selector: 'aa-root',
14 | imports: [AsyncPipe, RouterOutlet, HeaderComponent, FooterComponent],
15 | template: `
16 |
24 | `,
25 | styleUrls: ['./app.component.scss'],
26 | })
27 | export class AppComponent implements OnInit {
28 | private readonly router = inject(Router);
29 | private readonly destroyRef = inject(DestroyRef);
30 | private readonly authFacade = inject(AUTH_FACADE);
31 | private readonly configService = inject(ConfigService);
32 | private readonly googleAnalyticsService = inject(GoogleAnalyticsService);
33 |
34 | readonly version = this.configService.getVersion();
35 | readonly authUser$ = this.authFacade.authUser$;
36 |
37 | ngOnInit() {
38 | if (this.configService.isProd()) {
39 | this.setupGoogleAnalytics();
40 | }
41 | }
42 |
43 | protected onLogout() {
44 | this.authFacade.logout();
45 | }
46 |
47 | private setupGoogleAnalytics() {
48 | this.router.events
49 | .pipe(
50 | takeUntilDestroyed(this.destroyRef),
51 | filter(event => event instanceof NavigationEnd)
52 | )
53 | .subscribe(navigationEndEvent => {
54 | this.googleAnalyticsService.sendPageView(
55 | (navigationEndEvent as NavigationEnd).urlAfterRedirects
56 | );
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { provideHttpClient, withInterceptors } from '@angular/common/http';
2 | import {
3 | ApplicationConfig,
4 | provideExperimentalZonelessChangeDetection,
5 | } from '@angular/core';
6 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
7 | import { provideRouter } from '@angular/router';
8 |
9 | import { routes } from './app.routes';
10 | import { provideAuthStore, provideSetupStore, StoreType } from './app.store';
11 | import { authInterceptor } from './auth';
12 | import { fakeApiInterceptor } from './core/fake-api';
13 |
14 | // ⚠️ FIXME: choose one store and remove any packages in real app ⚠️
15 | const storeType = StoreType.Ngxs;
16 |
17 | export const appConfig: ApplicationConfig = {
18 | providers: [
19 | // Setup Angular
20 | provideExperimentalZonelessChangeDetection(),
21 | provideAnimationsAsync(),
22 |
23 | // Setup Store
24 | provideSetupStore(storeType),
25 |
26 | // Setup Interceptors
27 | provideHttpClient(
28 | withInterceptors([
29 | authInterceptor,
30 | // ⚠️ FIXME: remove it in real app ⚠️
31 | fakeApiInterceptor,
32 | ])
33 | ),
34 |
35 | // Setup Application
36 | provideAuthStore(storeType),
37 | provideRouter(routes),
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | import { authGuard } from './auth';
4 |
5 | export const routes: Routes = [
6 | // Redirect to home if root path
7 | { path: '', redirectTo: 'home', pathMatch: 'full' },
8 |
9 | {
10 | path: 'home',
11 | loadComponent: () => import('./features/home').then(c => c.HomeComponent),
12 | },
13 | {
14 | path: 'about',
15 | loadComponent: () => import('./features/about').then(c => c.AboutComponent),
16 | },
17 | {
18 | path: 'secured-feat',
19 | canActivate: [authGuard],
20 | loadComponent: () =>
21 | import('./features/secured-feat').then(c => c.SecuredFeatComponent),
22 | },
23 | {
24 | path: 'auth',
25 | loadChildren: () => import('./auth/auth.routes').then(c => c.authRoutes),
26 | },
27 |
28 | // Redirect to home if no route found
29 | { path: '**', redirectTo: 'home' },
30 | ];
31 |
--------------------------------------------------------------------------------
/src/app/app.store.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ⚠️ FIXME: choose one store and remove any unused packages in real app ⚠️
3 | * This file contains the store setup for the application.
4 | * It supports both Ngrx and Ngxs for the store management, but only one can be used at a time.
5 | * In real applications, you should choose one and remove the unused packages.
6 | */
7 |
8 | import { provideEffects } from '@ngrx/effects';
9 | import { provideRouterStore, routerReducer } from '@ngrx/router-store';
10 | import { provideStore as provideNgrxStore } from '@ngrx/store';
11 | import { provideStoreDevtools } from '@ngrx/store-devtools';
12 | import { withNgxsReduxDevtoolsPlugin } from '@ngxs/devtools-plugin';
13 | import {
14 | provideStore as provideNgxsStore,
15 | withNgxsDevelopmentOptions,
16 | } from '@ngxs/store';
17 |
18 | import { environment } from '../environments/environment';
19 |
20 | import { provideNgrxAuthStore, provideNgxsAuthStore } from './auth';
21 |
22 | const APP_NAME = 'Angular Authentication';
23 |
24 | export enum StoreType {
25 | Ngrx = 'ngrx',
26 | Ngxs = 'ngxs',
27 | }
28 |
29 | /**
30 | * Provides all the necessary store providers for setting up the store.
31 | * It supports both Ngrx and Ngxs.
32 | */
33 | export function provideSetupStore(storeType: StoreType) {
34 | const isDevToolsEnabled = !environment.production;
35 |
36 | const providers = {
37 | ngrx: [
38 | provideNgrxStore({ router: routerReducer }),
39 | provideRouterStore(),
40 | provideEffects(),
41 | isDevToolsEnabled ? provideStoreDevtools({ name: APP_NAME }) : [],
42 | ],
43 | ngxs: [
44 | provideNgxsStore(
45 | [],
46 | withNgxsReduxDevtoolsPlugin({
47 | name: APP_NAME,
48 | disabled: !isDevToolsEnabled,
49 | }),
50 | withNgxsDevelopmentOptions({
51 | warnOnUnhandledActions: true,
52 | })
53 | ),
54 | ],
55 | };
56 |
57 | return providers[storeType];
58 | }
59 |
60 | /**
61 | * Provides the authentication store for the application.
62 | * It supports both Ngrx and Ngxs.
63 | */
64 | export function provideAuthStore(storeType: StoreType) {
65 | const providers = {
66 | ngrx: provideNgrxAuthStore(),
67 | ngxs: provideNgxsAuthStore(),
68 | };
69 |
70 | return providers[storeType];
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/auth/auth.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | import { noAuthGuard } from './guards';
4 |
5 | export const authRoutes: Routes = [
6 | {
7 | path: 'login',
8 | canActivate: [noAuthGuard],
9 | loadComponent: () => import('./login/login.component').then(c => c.LoginComponent),
10 | },
11 | ];
12 |
--------------------------------------------------------------------------------
/src/app/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient, HttpParams } from '@angular/common/http';
2 | import { Injectable, inject } from '@angular/core';
3 | import { Observable, throwError } from 'rxjs';
4 |
5 | import { ConfigService, TokenStorageService } from '../core/services';
6 |
7 | import { AuthUser } from './models';
8 |
9 | export interface AccessData {
10 | token_type: 'Bearer';
11 | expires_in: number;
12 | access_token: string;
13 | refresh_token: string;
14 | }
15 |
16 | @Injectable({ providedIn: 'root' })
17 | export class AuthService {
18 | private readonly http = inject(HttpClient);
19 | private readonly configService = inject(ConfigService);
20 | private readonly tokenStorageService = inject(TokenStorageService);
21 |
22 | private readonly hostUrl = this.configService.getAPIUrl();
23 | private readonly clientId = this.configService.getAuthSettings().clientId;
24 | private readonly clientSecret = this.configService.getAuthSettings().secretId;
25 |
26 | /**
27 | * Performs a request with user credentials
28 | * in order to get auth tokens
29 | *
30 | * @param {string} username
31 | * @param {string} password
32 | * @returns Observable
33 | */
34 | login(username: string, password: string): Observable {
35 | return this.http.post(`${this.hostUrl}/api/auth/login`, {
36 | client_id: this.clientId,
37 | client_secret: this.clientSecret,
38 | grant_type: 'password',
39 | username,
40 | password,
41 | });
42 | }
43 |
44 | /**
45 | * Performs a request for logout authenticated user
46 | *
47 | * @param {('all' | 'allButCurrent' | 'current')} [clients='current']
48 | * @returns Observable
49 | */
50 | logout(clients: 'all' | 'allButCurrent' | 'current' = 'current'): Observable {
51 | const params = new HttpParams().append('clients', clients);
52 |
53 | return this.http.get(`${this.hostUrl}/api/auth/logout`, { params });
54 | }
55 |
56 | /**
57 | * Asks for a new access token given
58 | * the stored refresh token
59 | *
60 | * @returns {Observable}
61 | */
62 | refreshToken(): Observable {
63 | const refreshToken = this.tokenStorageService.getRefreshToken();
64 | if (!refreshToken) {
65 | return throwError(() => new Error('Refresh token does not exist'));
66 | }
67 |
68 | return this.http.post(`${this.hostUrl}/api/auth/login`, {
69 | client_id: this.clientId,
70 | client_secret: this.clientSecret,
71 | grant_type: 'refresh_token',
72 | refresh_token: refreshToken,
73 | });
74 | }
75 |
76 | /**
77 | * Returns authenticated user
78 | * based on saved access token
79 | *
80 | * @returns {Observable}
81 | */
82 | getAuthUser(): Observable {
83 | return this.http.get(`${this.hostUrl}/api/users/me`);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/auth/guards/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { inject } from '@angular/core';
2 | import {
3 | ActivatedRouteSnapshot,
4 | RouterStateSnapshot,
5 | createUrlTreeFromSnapshot,
6 | } from '@angular/router';
7 | import { map, take } from 'rxjs/operators';
8 |
9 | import { AUTH_FACADE } from '../tokens';
10 |
11 | export const authGuard = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
12 | const authFacade = inject(AUTH_FACADE);
13 |
14 | return authFacade.isLoggedIn$.pipe(
15 | take(1),
16 | map(isLoggedIn =>
17 | isLoggedIn
18 | ? // If the user is logged in, allow the route
19 | true
20 | : // Redirect to login page with return URL
21 | createUrlTreeFromSnapshot(route, ['/auth/login'], { returnUrl: state.url })
22 | )
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/app/auth/guards/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth.guard';
2 | export * from './no-auth.guard';
3 |
--------------------------------------------------------------------------------
/src/app/auth/guards/no-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { inject } from '@angular/core';
2 | import { ActivatedRouteSnapshot, createUrlTreeFromSnapshot } from '@angular/router';
3 | import { map, take } from 'rxjs/operators';
4 |
5 | import { AUTH_FACADE } from '../tokens';
6 |
7 | export const noAuthGuard = (route: ActivatedRouteSnapshot) => {
8 | const authFacade = inject(AUTH_FACADE);
9 |
10 | return authFacade.isLoggedIn$.pipe(
11 | take(1),
12 | map(isLoggedIn =>
13 | !isLoggedIn
14 | ? // If the user is not logged in, allow the route
15 | true
16 | : // Redirect to home page
17 | createUrlTreeFromSnapshot(route, ['/'])
18 | )
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/app/auth/index.ts:
--------------------------------------------------------------------------------
1 | export type { AuthUser, IAuthFacade } from './models';
2 | export { AUTH_FACADE } from './tokens';
3 | export { authGuard } from './guards';
4 | export { authInterceptor } from './interceptors';
5 |
6 | // Stores
7 | export { provideAuthStore as provideNgrxAuthStore } from './store/index.ngrx';
8 | export { provideAuthStore as provideNgxsAuthStore } from './store/index.ngxs';
9 |
--------------------------------------------------------------------------------
/src/app/auth/interceptors/auth.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | HttpErrorResponse,
3 | HttpEvent,
4 | HttpRequest,
5 | HttpHandlerFn,
6 | } from '@angular/common/http';
7 | import { inject } from '@angular/core';
8 | import { EMPTY, Observable, throwError } from 'rxjs';
9 | import { catchError } from 'rxjs/operators';
10 |
11 | import { TokenStorageService } from '../../core/services';
12 | import { AUTH_FACADE } from '../tokens';
13 |
14 | export function authInterceptor(
15 | req: HttpRequest,
16 | next: HttpHandlerFn
17 | ): Observable> {
18 | const authFacade = inject(AUTH_FACADE);
19 | const tokenStorageService = inject(TokenStorageService);
20 |
21 | const handle401 = () => {
22 | authFacade.logout();
23 | return EMPTY;
24 | };
25 |
26 | const accessToken = tokenStorageService.getAccessToken();
27 |
28 | if (accessToken) {
29 | // Add the Authorization header to the request
30 | req = req.clone({ setHeaders: { Authorization: `Bearer ${accessToken}` } });
31 | }
32 |
33 | return next(req).pipe(
34 | catchError((error: HttpErrorResponse) => {
35 | // try to avoid errors on logout
36 | // therefore we check the url path of '/auth/'
37 | const ignoreAPIs = ['/auth/'];
38 | if (ignoreAPIs.some(api => req.url.includes(api))) {
39 | return throwError(() => error);
40 | }
41 |
42 | // Handle global error status
43 | switch (error.status) {
44 | case 401:
45 | return handle401();
46 | // Add more error status handling here (e.g. 403)
47 | default:
48 | // Rethrow the error as is
49 | return throwError(() => error);
50 | }
51 | })
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/auth/interceptors/index.ts:
--------------------------------------------------------------------------------
1 | export { authInterceptor } from './auth.interceptor';
2 |
--------------------------------------------------------------------------------
/src/app/auth/login/login.component.html:
--------------------------------------------------------------------------------
1 | @if (vm$ | async; as vm) {
2 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/auth/login/login.component.scss:
--------------------------------------------------------------------------------
1 | .form-wrapper {
2 | @apply aa--mt-10 aa--w-10/12 md:aa--w-1/2;
3 |
4 | form {
5 | background-color: white;
6 | @apply aa--grid aa--gap-2;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/auth/login/login.component.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe } from '@angular/common';
2 | import { Component, inject } from '@angular/core';
3 | import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
4 | import { MatButtonModule } from '@angular/material/button';
5 | import { MatCardModule } from '@angular/material/card';
6 | import { MatFormFieldModule } from '@angular/material/form-field';
7 | import { MatInputModule } from '@angular/material/input';
8 | import { combineLatest } from 'rxjs';
9 |
10 | import { AUTH_FACADE } from '../tokens';
11 |
12 | @Component({
13 | selector: 'aa-login',
14 | imports: [
15 | AsyncPipe,
16 | MatButtonModule,
17 | MatCardModule,
18 | MatFormFieldModule,
19 | MatInputModule,
20 | ReactiveFormsModule,
21 | ],
22 | templateUrl: './login.component.html',
23 | styleUrls: ['./login.component.scss'],
24 | })
25 | export class LoginComponent {
26 | private readonly authFacade = inject(AUTH_FACADE);
27 |
28 | readonly loginForm = new FormGroup({
29 | username: new FormControl('', {
30 | validators: [Validators.required],
31 | nonNullable: true,
32 | }),
33 | password: new FormControl('', {
34 | validators: [Validators.required],
35 | nonNullable: true,
36 | }),
37 | });
38 |
39 | readonly vm$ = combineLatest({
40 | isLoading: this.authFacade.isLoadingLogin$,
41 | showLoginError: this.authFacade.hasLoginError$,
42 | });
43 |
44 | submit() {
45 | const { username, password } = this.loginForm.value;
46 | this.authFacade.login(username as string, password as string);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/auth/models/auth-facade.model.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 |
3 | import { AuthUser } from './auth-user.model';
4 |
5 | export interface IAuthFacade {
6 | readonly authUser$: Observable;
7 | readonly isLoggedIn$: Observable;
8 | readonly isLoadingLogin$: Observable;
9 | readonly hasLoginError$: Observable;
10 |
11 | login(username: string, password: string): void;
12 | logout(): void;
13 | getAuthUser(): void;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/auth/models/auth-state.model.ts:
--------------------------------------------------------------------------------
1 | import { AuthUser } from './auth-user.model';
2 |
3 | export enum TokenStatus {
4 | PENDING = 'PENDING',
5 | VALIDATING = 'VALIDATING',
6 | VALID = 'VALID',
7 | INVALID = 'INVALID',
8 | }
9 |
10 | export interface AuthStateModel {
11 | isLoggedIn: boolean;
12 | user?: AuthUser;
13 | accessTokenStatus: TokenStatus;
14 | refreshTokenStatus: TokenStatus;
15 | isLoadingLogin: boolean;
16 | hasLoginError: boolean;
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/auth/models/auth-user.model.ts:
--------------------------------------------------------------------------------
1 | export interface AuthUser {
2 | id: number;
3 | firstName: string;
4 | lastName: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/auth/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth-facade.model';
2 | export * from './auth-state.model';
3 | export * from './auth-user.model';
4 |
--------------------------------------------------------------------------------
/src/app/auth/store/index.ngrx.ts:
--------------------------------------------------------------------------------
1 | import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
2 | import { provideEffects } from '@ngrx/effects';
3 | import { provideState } from '@ngrx/store';
4 |
5 | import { AUTH_FACADE } from '../tokens';
6 |
7 | import {
8 | provideAuthInit,
9 | AuthEffects,
10 | AUTH_FEATURE_KEY,
11 | authReducer,
12 | NgrxAuthFacade,
13 | } from './ngrx';
14 |
15 | export function provideAuthStore(): EnvironmentProviders {
16 | return makeEnvironmentProviders([
17 | // Register Auth Store
18 | provideState(AUTH_FEATURE_KEY, authReducer),
19 | provideEffects(AuthEffects),
20 | provideAuthInit(),
21 | // Register Auth Facade
22 | {
23 | provide: AUTH_FACADE,
24 | useClass: NgrxAuthFacade,
25 | },
26 | ]);
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/auth/store/index.ngxs.ts:
--------------------------------------------------------------------------------
1 | import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
2 | import { provideStates } from '@ngxs/store';
3 |
4 | import { AUTH_FACADE } from '../tokens';
5 |
6 | import { AuthState, NgxsAuthFacade, provideAuthInit } from './ngxs';
7 |
8 | export function provideAuthStore(): EnvironmentProviders {
9 | return makeEnvironmentProviders([
10 | provideStates([AuthState]),
11 | provideAuthInit(),
12 | // Register Auth Facade
13 | {
14 | provide: AUTH_FACADE,
15 | useClass: NgxsAuthFacade,
16 | },
17 | ]);
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngrx/auth.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction, createActionGroup, emptyProps, props } from '@ngrx/store';
2 |
3 | import { AuthUser } from '../../models';
4 |
5 | // Login
6 | export const LoginActions = createActionGroup({
7 | source: 'Auth: Login',
8 | events: {
9 | request: props<{ username: string; password: string }>(),
10 | success: emptyProps(),
11 | failure: props<{ error: Error }>(),
12 | },
13 | });
14 |
15 | // Logout
16 | export const LogoutAction = createAction('[Auth] Logout');
17 |
18 | // Auth User: me
19 | export const AuthUserActions = createActionGroup({
20 | source: 'Auth: Auth User',
21 | events: {
22 | request: emptyProps(),
23 | success: props<{ user: AuthUser }>(),
24 | failure: emptyProps(),
25 | },
26 | });
27 |
28 | // Refresh token
29 | export const RefreshTokenActions = createActionGroup({
30 | source: 'Auth: Refresh Token',
31 | events: {
32 | request: emptyProps(),
33 | success: emptyProps(),
34 | failure: emptyProps(),
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngrx/auth.effects.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, inject } from '@angular/core';
2 | import { ActivatedRoute, Router } from '@angular/router';
3 | import { Actions, createEffect, ofType } from '@ngrx/effects';
4 | import { of } from 'rxjs';
5 | import { catchError, exhaustMap, finalize, map, tap } from 'rxjs/operators';
6 |
7 | import { TokenStorageService } from '../../../core/services';
8 | import { AuthService } from '../../auth.service';
9 |
10 | import {
11 | AuthUserActions,
12 | LoginActions,
13 | LogoutAction,
14 | RefreshTokenActions,
15 | } from './auth.actions';
16 |
17 | @Injectable()
18 | export class AuthEffects {
19 | private readonly router = inject(Router);
20 | private readonly actions$ = inject(Actions);
21 | private readonly authService = inject(AuthService);
22 | private readonly activatedRoute = inject(ActivatedRoute);
23 | private readonly tokenStorageService = inject(TokenStorageService);
24 |
25 | readonly login$ = createEffect(() => {
26 | return this.actions$.pipe(
27 | ofType(LoginActions.request),
28 | exhaustMap(credentials =>
29 | this.authService.login(credentials.username, credentials.password).pipe(
30 | map(data => {
31 | // save tokens
32 | this.tokenStorageService.saveTokens(data.access_token, data.refresh_token);
33 | // trigger login success action
34 | return LoginActions.success();
35 | }),
36 | catchError(error => of(LoginActions.failure({ error })))
37 | )
38 | )
39 | );
40 | });
41 |
42 | readonly onLoginSuccess$ = createEffect(() => {
43 | return this.actions$.pipe(
44 | ofType(LoginActions.success),
45 | map(() => {
46 | // redirect to return url or home
47 | this.router.navigateByUrl(
48 | this.activatedRoute.snapshot.queryParams['returnUrl'] || '/'
49 | );
50 | return AuthUserActions.request();
51 | })
52 | );
53 | });
54 |
55 | readonly logout$ = createEffect(
56 | () => {
57 | return this.actions$.pipe(
58 | ofType(LogoutAction),
59 | exhaustMap(() => {
60 | this.router.navigateByUrl('/');
61 | return this.authService
62 | .logout()
63 | .pipe(finalize(() => this.tokenStorageService.removeTokens()));
64 | })
65 | );
66 | },
67 | { dispatch: false }
68 | );
69 |
70 | readonly getUser$ = createEffect(() => {
71 | return this.actions$.pipe(
72 | ofType(RefreshTokenActions.success, AuthUserActions.request),
73 | exhaustMap(() =>
74 | this.authService.getAuthUser().pipe(
75 | map(user => AuthUserActions.success({ user })),
76 | catchError(() => of(AuthUserActions.failure()))
77 | )
78 | )
79 | );
80 | });
81 |
82 | readonly refreshToken$ = createEffect(() => {
83 | return this.actions$.pipe(
84 | ofType(RefreshTokenActions.request),
85 | exhaustMap(() =>
86 | this.authService.refreshToken().pipe(
87 | map(data => {
88 | // save tokens
89 | this.tokenStorageService.saveTokens(data.access_token, data.refresh_token);
90 | // trigger refresh token success action
91 | return RefreshTokenActions.success();
92 | }),
93 | catchError(() => of(RefreshTokenActions.failure()))
94 | )
95 | )
96 | );
97 | });
98 |
99 | readonly onLoginOrRefreshTokenFailure$ = createEffect(
100 | () => {
101 | return this.actions$.pipe(
102 | ofType(LoginActions.failure, RefreshTokenActions.failure),
103 | tap(() => this.tokenStorageService.removeTokens())
104 | );
105 | },
106 | { dispatch: false }
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngrx/auth.facade.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, inject } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 |
4 | import { IAuthFacade } from '../../models';
5 |
6 | import { LogoutAction, LoginActions, AuthUserActions } from './auth.actions';
7 | import * as AuthSelectors from './auth.selectors';
8 |
9 | @Injectable()
10 | export class NgrxAuthFacade implements IAuthFacade {
11 | private readonly store = inject(Store);
12 |
13 | readonly authUser$ = this.store.select(AuthSelectors.selectAuthUser);
14 | readonly isLoggedIn$ = this.store.select(AuthSelectors.selectIsLoggedIn);
15 | readonly isLoadingLogin$ = this.store.select(AuthSelectors.selectIsLoadingLogin);
16 | readonly hasLoginError$ = this.store.select(AuthSelectors.selectLoginError);
17 |
18 | login(username: string, password: string) {
19 | this.store.dispatch(LoginActions.request({ username, password }));
20 | }
21 |
22 | logout() {
23 | this.store.dispatch(LogoutAction());
24 | }
25 |
26 | getAuthUser() {
27 | this.store.dispatch(AuthUserActions.request());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngrx/auth.reducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, createReducer, on } from '@ngrx/store';
2 |
3 | import { AuthStateModel, TokenStatus } from '../../models';
4 |
5 | import {
6 | AuthUserActions,
7 | LoginActions,
8 | LogoutAction,
9 | RefreshTokenActions,
10 | } from './auth.actions';
11 |
12 | export const AUTH_FEATURE_KEY = 'auth';
13 |
14 | export interface AuthPartialState {
15 | readonly [AUTH_FEATURE_KEY]: AuthStateModel;
16 | }
17 |
18 | export const initialState: AuthStateModel = {
19 | isLoggedIn: false,
20 | user: undefined,
21 | accessTokenStatus: TokenStatus.PENDING,
22 | refreshTokenStatus: TokenStatus.PENDING,
23 | isLoadingLogin: false,
24 | hasLoginError: false,
25 | };
26 |
27 | const reducer = createReducer(
28 | initialState,
29 |
30 | // Login
31 | on(
32 | LoginActions.request,
33 | (state): AuthStateModel => ({
34 | ...state,
35 | accessTokenStatus: TokenStatus.VALIDATING,
36 | isLoadingLogin: true,
37 | hasLoginError: false,
38 | })
39 | ),
40 |
41 | // Refresh token
42 | on(
43 | RefreshTokenActions.request,
44 | (state): AuthStateModel => ({
45 | ...state,
46 | refreshTokenStatus: TokenStatus.VALIDATING,
47 | })
48 | ),
49 |
50 | // Login & Refresh token
51 | on(
52 | LoginActions.success,
53 | RefreshTokenActions.success,
54 | (state): AuthStateModel => ({
55 | ...state,
56 | isLoggedIn: true,
57 | isLoadingLogin: false,
58 | accessTokenStatus: TokenStatus.VALID,
59 | refreshTokenStatus: TokenStatus.VALID,
60 | })
61 | ),
62 | on(
63 | LoginActions.failure,
64 | RefreshTokenActions.failure,
65 | (state, action): AuthStateModel => ({
66 | ...state,
67 | isLoadingLogin: false,
68 | accessTokenStatus: TokenStatus.INVALID,
69 | refreshTokenStatus: TokenStatus.INVALID,
70 | hasLoginError: action.type === LoginActions.failure.type && !!action.error,
71 | })
72 | ),
73 |
74 | // Logout
75 | on(
76 | LogoutAction,
77 | (): AuthStateModel => ({
78 | ...initialState,
79 | })
80 | ),
81 |
82 | // Auth user
83 | on(
84 | AuthUserActions.success,
85 | (state, action): AuthStateModel => ({
86 | ...state,
87 | user: action.user,
88 | })
89 | ),
90 | on(
91 | AuthUserActions.failure,
92 | (): AuthStateModel => ({
93 | ...initialState,
94 | })
95 | )
96 | );
97 |
98 | export function authReducer(
99 | state: AuthStateModel | undefined,
100 | action: Action
101 | ): AuthStateModel {
102 | return reducer(state, action);
103 | }
104 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngrx/auth.selectors.ts:
--------------------------------------------------------------------------------
1 | import { createFeatureSelector, createSelector } from '@ngrx/store';
2 |
3 | import { AuthStateModel } from '../../models';
4 |
5 | import { AUTH_FEATURE_KEY } from './auth.reducer';
6 |
7 | export const selectAuth = createFeatureSelector(AUTH_FEATURE_KEY);
8 |
9 | export const selectIsLoggedIn = createSelector(selectAuth, state => state.isLoggedIn);
10 |
11 | export const selectLoginError = createSelector(selectAuth, state => state.hasLoginError);
12 |
13 | export const selectIsLoadingLogin = createSelector(
14 | selectAuth,
15 | state => state.isLoadingLogin
16 | );
17 |
18 | export const selectAuthUser = createSelector(selectAuth, state => state.user);
19 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngrx/index.ts:
--------------------------------------------------------------------------------
1 | import { EnvironmentProviders, inject, provideAppInitializer } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 | import { lastValueFrom } from 'rxjs';
4 | import { filter, take } from 'rxjs/operators';
5 |
6 | import { AuthStateModel, TokenStatus } from '../../models';
7 |
8 | import { RefreshTokenActions } from './auth.actions';
9 | import * as AuthSelectors from './auth.selectors';
10 |
11 | export { AuthEffects } from './auth.effects';
12 | export { NgrxAuthFacade } from './auth.facade';
13 | export * from './auth.reducer';
14 |
15 | const initializeAuth = () => {
16 | const store = inject>(Store);
17 |
18 | store.dispatch(RefreshTokenActions.request());
19 |
20 | const authState$ = store.select(AuthSelectors.selectAuth).pipe(
21 | filter(
22 | auth =>
23 | auth.refreshTokenStatus === TokenStatus.INVALID ||
24 | (auth.refreshTokenStatus === TokenStatus.VALID && !!auth.user)
25 | ),
26 | take(1)
27 | );
28 |
29 | return lastValueFrom(authState$);
30 | };
31 |
32 | export const provideAuthInit = (): EnvironmentProviders => {
33 | return provideAppInitializer(initializeAuth);
34 | };
35 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngxs/auth.actions.ts:
--------------------------------------------------------------------------------
1 | export class Login {
2 | static readonly type = '[Auth] Login';
3 |
4 | constructor(
5 | public username: string,
6 | public password: string
7 | ) {}
8 | }
9 |
10 | export class Logout {
11 | static readonly type = '[Auth] Logout';
12 | }
13 |
14 | export class FetchAuthUser {
15 | static readonly type = '[Auth] Fetch Auth User';
16 | }
17 |
18 | export class RefreshToken {
19 | static readonly type = '[Auth] Refresh Token';
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngxs/auth.facade.ts:
--------------------------------------------------------------------------------
1 | import { inject, Injectable } from '@angular/core';
2 | import { Store } from '@ngxs/store';
3 |
4 | import { IAuthFacade } from '../../models';
5 |
6 | import { FetchAuthUser, Login, Logout } from './auth.actions';
7 | import { AuthSelectors } from './auth.selectors';
8 |
9 | @Injectable()
10 | export class NgxsAuthFacade implements IAuthFacade {
11 | private readonly store = inject(Store);
12 |
13 | readonly authUser$ = this.store.select(AuthSelectors.authUser);
14 | readonly isLoggedIn$ = this.store.select(AuthSelectors.isLoggedIn);
15 | readonly isLoadingLogin$ = this.store.select(AuthSelectors.isLoadingLogin);
16 | readonly hasLoginError$ = this.store.select(AuthSelectors.loginError);
17 |
18 | login(username: string, password: string) {
19 | this.store.dispatch(new Login(username, password));
20 | }
21 |
22 | logout() {
23 | this.store.dispatch(new Logout());
24 | }
25 |
26 | getAuthUser() {
27 | this.store.dispatch(new FetchAuthUser());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngxs/auth.selectors.ts:
--------------------------------------------------------------------------------
1 | import { Selector } from '@ngxs/store';
2 |
3 | import { AuthStateModel } from '../../models';
4 |
5 | import { AuthState } from './auth.state';
6 |
7 | export class AuthSelectors {
8 | @Selector([AuthState])
9 | static auth(state: AuthStateModel) {
10 | return state;
11 | }
12 |
13 | @Selector([AuthState])
14 | static isLoggedIn(state: AuthStateModel) {
15 | return state.isLoggedIn;
16 | }
17 |
18 | @Selector([AuthState])
19 | static loginError(state: AuthStateModel) {
20 | return state.hasLoginError;
21 | }
22 |
23 | @Selector([AuthState])
24 | static isLoadingLogin(state: AuthStateModel) {
25 | return state.isLoadingLogin;
26 | }
27 |
28 | @Selector([AuthState])
29 | static authUser(state: AuthStateModel) {
30 | return state.user;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngxs/auth.state.ts:
--------------------------------------------------------------------------------
1 | import { inject, Injectable } from '@angular/core';
2 | import { ActivatedRoute, Router } from '@angular/router';
3 | import { Action, State, StateContext } from '@ngxs/store';
4 | import { catchError, finalize, switchMap, tap, throwError } from 'rxjs';
5 |
6 | import { TokenStorageService } from '../../../core/services';
7 | import { AuthService } from '../../auth.service';
8 | import { AuthStateModel, TokenStatus } from '../../models';
9 |
10 | import { Login, Logout, RefreshToken, FetchAuthUser } from './auth.actions';
11 |
12 | const AUTH_FEATURE_KEY = 'auth';
13 |
14 | const initialState: AuthStateModel = {
15 | isLoggedIn: false,
16 | user: undefined,
17 | accessTokenStatus: TokenStatus.PENDING,
18 | refreshTokenStatus: TokenStatus.PENDING,
19 | isLoadingLogin: false,
20 | hasLoginError: false,
21 | };
22 |
23 | @State({
24 | name: AUTH_FEATURE_KEY,
25 | defaults: initialState,
26 | })
27 | @Injectable()
28 | export class AuthState {
29 | private readonly router = inject(Router);
30 | private readonly authService = inject(AuthService);
31 | private readonly activatedRoute = inject(ActivatedRoute);
32 | private readonly tokenStorageService = inject(TokenStorageService);
33 |
34 | @Action(Login)
35 | login(ctx: StateContext, action: Login) {
36 | ctx.patchState({
37 | accessTokenStatus: TokenStatus.VALIDATING,
38 | isLoadingLogin: true,
39 | hasLoginError: false,
40 | });
41 |
42 | return this.authService.login(action.username, action.password).pipe(
43 | switchMap(data => {
44 | this.tokenStorageService.saveTokens(data.access_token, data.refresh_token);
45 |
46 | ctx.patchState({
47 | isLoggedIn: true,
48 | isLoadingLogin: false,
49 | accessTokenStatus: TokenStatus.VALID,
50 | refreshTokenStatus: TokenStatus.VALID,
51 | });
52 |
53 | // Redirect to return url or home
54 | const returnUrl = this.activatedRoute.snapshot.queryParams['returnUrl'] || '/';
55 | this.router.navigateByUrl(returnUrl);
56 |
57 | return ctx.dispatch(new FetchAuthUser());
58 | }),
59 | catchError(error => {
60 | ctx.patchState({
61 | isLoadingLogin: false,
62 | accessTokenStatus: TokenStatus.INVALID,
63 | refreshTokenStatus: TokenStatus.INVALID,
64 | hasLoginError: true,
65 | });
66 |
67 | this.tokenStorageService.removeTokens();
68 |
69 | return throwError(() => error);
70 | })
71 | );
72 | }
73 |
74 | @Action(Logout)
75 | logout(ctx: StateContext) {
76 | ctx.setState({ ...initialState });
77 |
78 | this.router.navigateByUrl('/');
79 |
80 | return this.authService
81 | .logout()
82 | .pipe(finalize(() => this.tokenStorageService.removeTokens()));
83 | }
84 |
85 | @Action(FetchAuthUser)
86 | authUserRequest(ctx: StateContext) {
87 | return this.authService.getAuthUser().pipe(
88 | tap(user => ctx.patchState({ user })),
89 | catchError(error => {
90 | ctx.setState({ ...initialState });
91 | return throwError(() => error);
92 | })
93 | );
94 | }
95 |
96 | @Action(RefreshToken)
97 | refreshTokenRequest(ctx: StateContext) {
98 | ctx.patchState({ refreshTokenStatus: TokenStatus.VALIDATING });
99 |
100 | return this.authService.refreshToken().pipe(
101 | switchMap(data => {
102 | this.tokenStorageService.saveTokens(data.access_token, data.refresh_token);
103 |
104 | ctx.patchState({
105 | isLoggedIn: true,
106 | isLoadingLogin: false,
107 | accessTokenStatus: TokenStatus.VALID,
108 | refreshTokenStatus: TokenStatus.VALID,
109 | });
110 |
111 | return ctx.dispatch(new FetchAuthUser());
112 | }),
113 | catchError(error => {
114 | ctx.patchState({
115 | isLoadingLogin: false,
116 | accessTokenStatus: TokenStatus.INVALID,
117 | refreshTokenStatus: TokenStatus.INVALID,
118 | });
119 |
120 | this.tokenStorageService.removeTokens();
121 |
122 | return throwError(() => error);
123 | })
124 | );
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/app/auth/store/ngxs/index.ts:
--------------------------------------------------------------------------------
1 | import { EnvironmentProviders, inject, provideAppInitializer } from '@angular/core';
2 | import { Store } from '@ngxs/store';
3 | import { lastValueFrom } from 'rxjs';
4 | import { filter, take } from 'rxjs/operators';
5 |
6 | import { TokenStatus } from '../../models';
7 |
8 | import { RefreshToken } from './auth.actions';
9 | import { AuthSelectors } from './auth.selectors';
10 | export { NgxsAuthFacade } from './auth.facade';
11 | export { AuthState } from './auth.state';
12 |
13 | const initializeAuth = () => {
14 | const store = inject(Store);
15 |
16 | store.dispatch(new RefreshToken());
17 |
18 | const authState$ = store.select(AuthSelectors.auth).pipe(
19 | filter(
20 | auth =>
21 | auth.refreshTokenStatus === TokenStatus.INVALID ||
22 | (auth.refreshTokenStatus === TokenStatus.VALID && !!auth.user)
23 | ),
24 | take(1)
25 | );
26 |
27 | return lastValueFrom(authState$);
28 | };
29 |
30 | export const provideAuthInit = (): EnvironmentProviders => {
31 | return provideAppInitializer(initializeAuth);
32 | };
33 |
--------------------------------------------------------------------------------
/src/app/auth/tokens/auth-facade.token.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 |
3 | import { IAuthFacade } from '../models';
4 |
5 | export const AUTH_FACADE = new InjectionToken('AUTH_FACADE');
6 |
--------------------------------------------------------------------------------
/src/app/auth/tokens/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth-facade.token';
2 |
--------------------------------------------------------------------------------
/src/app/core/fake-api/db.data.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: number;
3 | firstName: string;
4 | lastName: string;
5 | username: string;
6 | password: string;
7 | accessToken: string;
8 | refreshToken: string;
9 | }
10 |
11 | export const USERS: User[] = [
12 | {
13 | id: 1,
14 | firstName: 'Admin',
15 | lastName: 'Demo',
16 | username: 'admin',
17 | password: 'demo',
18 | accessToken: 'valid-jwt-access-token-1',
19 | refreshToken: 'valid-jwt-refresh-token-1',
20 | },
21 | {
22 | id: 2,
23 | firstName: 'Nikos',
24 | lastName: 'Anifantis',
25 | username: 'nikos',
26 | password: '1234',
27 | accessToken: 'valid-jwt-access-token-2',
28 | refreshToken: 'valid-jwt-refresh-token-2',
29 | },
30 | {
31 | id: 3,
32 | firstName: 'John',
33 | lastName: 'Doe',
34 | username: 'john',
35 | password: '4321',
36 | accessToken: 'valid-jwt-access-token-3',
37 | refreshToken: 'valid-jwt-refresh-token-3',
38 | },
39 | ];
40 |
--------------------------------------------------------------------------------
/src/app/core/fake-api/fake-api.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { HttpEvent, HttpRequest } from '@angular/common/http';
2 | import { Observable, catchError, delay, tap, throwError } from 'rxjs';
3 |
4 | import { FakeApi } from './fake-api';
5 |
6 | export function fakeApiInterceptor(
7 | request: HttpRequest
8 | ): Observable> {
9 | const { method, url, body } = request;
10 | console.log('[FakeApiInterceptor] Request ⏩');
11 | console.table({ method, url, body });
12 |
13 | return new FakeApi(request as HttpRequest>)
14 | .handleRequest()
15 | .pipe(
16 | delay(200), // delay to simulate server latency
17 | tap(response => {
18 | const { status, url, body } = response;
19 | console.log('[FakeApiInterceptor] Response success ✅');
20 | console.table({ status, url, body });
21 | }),
22 | catchError(error => {
23 | console.error('[FakeApiInterceptor] Response error ❌');
24 | console.error(error);
25 | return throwError(() => error);
26 | })
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/core/fake-api/fake-api.ts:
--------------------------------------------------------------------------------
1 | import { HttpRequest, HttpResponse } from '@angular/common/http';
2 | import { Observable, of, throwError } from 'rxjs';
3 |
4 | import { USERS } from './db.data';
5 |
6 | type RequestHandlers = Record HttpResponse>>;
7 |
8 | export class FakeApi {
9 | private usersDB = new UsersDB();
10 |
11 | constructor(private request: HttpRequest>) {}
12 |
13 | handleRequest(): Observable> {
14 | const requestsMapHandlers: RequestHandlers = {
15 | POST: {
16 | '/api/auth/login': () => this.handleLogin(),
17 | },
18 | GET: {
19 | '/api/auth/logout': () => this.handleLogout(),
20 | '/api/users/me': () => this.handleMe(),
21 | },
22 | };
23 |
24 | const { method, url } = this.request;
25 | const handler = requestsMapHandlers[method][url];
26 |
27 | if (handler) {
28 | const response = handler();
29 | return response.status < 400 ? of(response) : throwError(() => response);
30 | }
31 |
32 | return throwError(() => this.respond400Error(`Cannot ${method} ${url}`));
33 | }
34 |
35 | private handleLogin(): HttpResponse {
36 | const { body } = this.request;
37 | if (!body) return this.respond400Error();
38 |
39 | // validate grant type
40 | const grantType = body['grant_type'];
41 | if (!grantType) return this.respond400Error('Grant type is missing');
42 |
43 | if (grantType === 'password') {
44 | return this.handleLoginPassword(body);
45 | } else if (grantType === 'refresh_token') {
46 | return this.handleLoginRefreshToken(body);
47 | }
48 |
49 | return this.respond400Error('Grant type is incorrect');
50 | }
51 |
52 | private handleLoginPassword(body: Record): HttpResponse {
53 | // handle login for password grant type
54 | const { username, password } = body;
55 | if (!username || !password) {
56 | return this.respond400Error('Username or password is missing');
57 | }
58 |
59 | const user = this.usersDB.findByUsernameAndPassword(
60 | username as string,
61 | password as string
62 | );
63 |
64 | if (!user) return this.respond400Error('Username or password is incorrect');
65 |
66 | return this.respondSuccess({
67 | token_type: 'Bearer',
68 | expires_in: 86399,
69 | access_token: user.accessToken,
70 | refresh_token: user.refreshToken,
71 | });
72 | }
73 |
74 | private handleLoginRefreshToken(body: Record): HttpResponse {
75 | // handle login for refresh token grant type
76 | const refreshToken = body['refresh_token'];
77 | if (!refreshToken) return this.respond400Error('Refresh token is missing');
78 |
79 | const user = this.usersDB.findByRefreshToken(refreshToken as string);
80 | if (!user) return this.respond400Error('Refresh token is incorrect');
81 |
82 | return this.respondSuccess({
83 | token_type: 'Bearer',
84 | expires_in: 86399,
85 | access_token: user.accessToken,
86 | refresh_token: user.refreshToken,
87 | });
88 | }
89 |
90 | private handleMe(): HttpResponse {
91 | const { headers } = this.request;
92 | const accessToken = headers.get('Authorization')?.split(' ')[1];
93 | if (!accessToken) return this.respond400Error('Access token is missing');
94 |
95 | const user = this.usersDB.findByAccessToken(accessToken);
96 | if (!user) return this.respond400Error('Access token is incorrect');
97 |
98 | return this.respondSuccess({
99 | id: user.id,
100 | username: user.username,
101 | firstName: user.firstName,
102 | lastName: user.lastName,
103 | });
104 | }
105 |
106 | private handleLogout(): HttpResponse {
107 | return this.respondSuccess({});
108 | }
109 |
110 | private respondSuccess(body: unknown): HttpResponse {
111 | const { headers, url } = this.request;
112 | return new HttpResponse({ status: 200, headers, url, body });
113 | }
114 |
115 | private respond400Error(message = 'Bad request'): HttpResponse {
116 | return new HttpResponse({
117 | status: 400,
118 | body: { message },
119 | });
120 | }
121 | }
122 |
123 | class UsersDB {
124 | findAll() {
125 | return USERS;
126 | }
127 |
128 | findByUsernameAndPassword(username: string, password: string) {
129 | return this.findAll().find(
130 | user => user.username === username && user.password === password
131 | );
132 | }
133 |
134 | findByAccessToken(accessToken: string) {
135 | return this.findAll().find(user => user.accessToken === accessToken);
136 | }
137 |
138 | findByRefreshToken(refreshToken: string) {
139 | return this.findAll().find(user => user.refreshToken === refreshToken);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/app/core/fake-api/index.ts:
--------------------------------------------------------------------------------
1 | export { fakeApiInterceptor } from './fake-api.interceptor';
2 | export { USERS } from './db.data';
3 |
--------------------------------------------------------------------------------
/src/app/core/services/config.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { environment } from '../../../environments/environment';
4 |
5 | type AppEnv = typeof environment;
6 |
7 | @Injectable({ providedIn: 'root' })
8 | export class ConfigService {
9 | /**
10 | * Returns environment config of application
11 | */
12 | getEnvironment(): AppEnv {
13 | return environment;
14 | }
15 |
16 | /**
17 | * Indicates whether the apps is running in production mode
18 | *
19 | * @return {*} {boolean}
20 | */
21 | isProd(): boolean {
22 | return environment.production;
23 | }
24 |
25 | /**
26 | * Returns app's version
27 | */
28 | getVersion(): string {
29 | return environment.appVersion;
30 | }
31 |
32 | /**
33 | * Returns the server's host url
34 | */
35 | getAPIUrl(): string {
36 | return environment?.apiUrl ?? '';
37 | }
38 |
39 | /**
40 | * Returns configuration for auth client and secret
41 | */
42 | getAuthSettings(): AppEnv['settings']['auth'] {
43 | return environment?.settings?.auth;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/core/services/google-analytics.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
4 | type Gtag = any;
5 |
6 | declare const gtag: Gtag;
7 | const GOOGLE_ANALYTICS_ID = 'UA-217340656-1';
8 |
9 | @Injectable({ providedIn: 'root' })
10 | export class GoogleAnalyticsService {
11 | protected gtag: Gtag;
12 |
13 | constructor() {
14 | if (typeof gtag !== 'undefined') {
15 | this.gtag = gtag;
16 | }
17 | }
18 |
19 | sendEvent = (
20 | eventName: string,
21 | eventCategory: string,
22 | eventAction: string,
23 | eventLabel: string | null = null,
24 | eventValue: number | null = null
25 | ) => {
26 | if (!this.gtag) {
27 | return;
28 | }
29 |
30 | this.gtag('event', eventName, {
31 | eventCategory,
32 | eventLabel,
33 | eventAction,
34 | eventValue,
35 | });
36 | };
37 |
38 | sendPageView(url: string) {
39 | if (!this.gtag) {
40 | return;
41 | }
42 |
43 | this.gtag('config', GOOGLE_ANALYTICS_ID, { page_path: url });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/core/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config.service';
2 | export * from './google-analytics.service';
3 | export * from './local-storage.service';
4 | export * from './token-storage.service';
5 |
--------------------------------------------------------------------------------
/src/app/core/services/local-storage.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable({ providedIn: 'root' })
4 | export class LocalStorageService {
5 | static readonly APP_PREFIX = 'NG-AUTH-';
6 |
7 | /**
8 | * Sets item in local storage
9 | *
10 | * @param {string} key
11 | * @param {unknown} value
12 | */
13 | setItem(key: string, value: unknown) {
14 | try {
15 | localStorage.setItem(
16 | `${LocalStorageService.APP_PREFIX}${key}`,
17 | JSON.stringify(value)
18 | );
19 | } catch {
20 | localStorage.setItem(`${LocalStorageService.APP_PREFIX}${key}`, value as string);
21 | }
22 | }
23 |
24 | /**
25 | * Gets item from local storage by key
26 | *
27 | * @param {string} key
28 | * @return {*} {unknown}
29 | */
30 | getItem(key: string): unknown {
31 | const value = localStorage.getItem(`${LocalStorageService.APP_PREFIX}${key}`);
32 | try {
33 | return JSON.parse(value as string);
34 | } catch {
35 | return value;
36 | }
37 | }
38 |
39 | /**
40 | * Removes item from local storage by key
41 | *
42 | * @param {string} key
43 | */
44 | removeItem(key: string) {
45 | localStorage.removeItem(`${LocalStorageService.APP_PREFIX}${key}`);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/core/services/token-storage.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, inject } from '@angular/core';
2 |
3 | import { ConfigService } from './config.service';
4 | import { LocalStorageService } from './local-storage.service';
5 |
6 | @Injectable({ providedIn: 'root' })
7 | export class TokenStorageService {
8 | private readonly configService = inject(ConfigService);
9 | private readonly localStorageService = inject(LocalStorageService);
10 |
11 | private readonly accessTokenKey =
12 | this.configService.getAuthSettings().accessTokenKey || 'accessToken';
13 | private readonly refreshTokenKey =
14 | this.configService.getAuthSettings().refreshTokenKey || 'refreshToken';
15 |
16 | getAccessToken(): string {
17 | return this.localStorageService.getItem(this.accessTokenKey) as string;
18 | }
19 |
20 | saveAccessToken(token: string) {
21 | this.localStorageService.setItem(this.accessTokenKey, token);
22 | }
23 |
24 | getRefreshToken(): string {
25 | return this.localStorageService.getItem(this.refreshTokenKey) as string;
26 | }
27 |
28 | saveRefreshToken(token: string) {
29 | this.localStorageService.setItem(this.refreshTokenKey, token);
30 | }
31 |
32 | saveTokens(accessToken: string, refreshToken: string) {
33 | this.saveAccessToken(accessToken);
34 | this.saveRefreshToken(refreshToken);
35 | }
36 |
37 | removeTokens() {
38 | this.localStorageService.removeItem(this.accessTokenKey);
39 | this.localStorageService.removeItem(this.refreshTokenKey);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/features/about/about.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello
4 | ng
5 | Friends!
6 |
7 |
8 |
9 | I'm
10 | Nikos Anifantis
11 | , thanks for visiting this repository.
12 |
13 |
14 | Nothing special here, I just wanted to say hi! 👋
15 |
16 |
17 |
18 | Support ❤️🙏
19 |
20 |
41 |
42 |
--------------------------------------------------------------------------------
/src/app/features/about/about.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { MatButtonModule } from '@angular/material/button';
3 |
4 | import { IconModule } from '../../shared/ui/icon';
5 |
6 | @Component({
7 | selector: 'aa-about',
8 | imports: [MatButtonModule, IconModule],
9 | templateUrl: './about.component.html',
10 | })
11 | export class AboutComponent {}
12 |
--------------------------------------------------------------------------------
/src/app/features/about/index.ts:
--------------------------------------------------------------------------------
1 | export * from './about.component';
2 |
--------------------------------------------------------------------------------
/src/app/features/home/features.data.ts:
--------------------------------------------------------------------------------
1 | export interface Feature {
2 | name: string;
3 | description: string;
4 | link: string | null;
5 | github: string | null;
6 | docs: string | null;
7 | }
8 |
9 | export const features: Feature[] = [
10 | {
11 | name: 'Angular',
12 | description: `The modern web developer's platform.`,
13 | link: 'https://angular.dev/',
14 | github: 'https://github.com/angular/angular',
15 | docs: 'https://angular.dev/overview',
16 | },
17 | {
18 | name: 'NgRx',
19 | description: 'Reactive State for Angular.',
20 | link: 'https://ngrx.io/',
21 | github: 'https://github.com/ngrx/platform',
22 | docs: 'https://ngrx.io/docs',
23 | },
24 | {
25 | name: 'NGXS',
26 | description: 'NGXS is a state management pattern + library for Angular.',
27 | link: 'https://www.ngxs.io/',
28 | github: 'https://github.com/ngxs/store',
29 | docs: 'https://www.ngxs.io',
30 | },
31 | {
32 | name: 'Standalone Components',
33 | description: 'A simplified way to build Angular applications.',
34 | link: null,
35 | github: null,
36 | docs: 'https://angular.dev/guide/components',
37 | },
38 | {
39 | name: 'Zoneless',
40 | description: 'Angular without ZoneJS.',
41 | link: null,
42 | github: null,
43 | docs: 'https://angular.dev/guide/experimental/zoneless',
44 | },
45 | {
46 | name: 'RxJS',
47 | description: 'Reactive Extensions Library for JavaScript.',
48 | link: 'https://rxjs.dev/',
49 | github: 'https://github.com/ReactiveX/rxjs',
50 | docs: 'https://rxjs.dev/guide/overview',
51 | },
52 | {
53 | name: 'Angular Material',
54 | description: 'Material Design components for Angular.',
55 | link: 'https://material.angular.io/',
56 | github: 'https://github.com/angular/components',
57 | docs: 'https://material.angular.io/guide/getting-started',
58 | },
59 | {
60 | name: 'Tailwindcss',
61 | description: 'A utility-first CSS framework for rapid UI development.',
62 | link: 'https://tailwindcss.com/',
63 | github: 'https://github.com/tailwindlabs/tailwindcss',
64 | docs: 'https://tailwindcss.com/docs',
65 | },
66 | {
67 | name: 'Lazy loading',
68 | description: 'Faster startup time with lazy loaded feature modules.',
69 | link: null,
70 | github: null,
71 | docs: 'https://angular.dev/guide/routing/common-router-tasks#lazy-loading',
72 | },
73 | {
74 | name: 'Font Awesome 5',
75 | description: 'Icons and typefaces in a single place.',
76 | link: 'https://fontawesome.com/',
77 | github: 'https://github.com/FortAwesome/angular-fontawesome',
78 | docs: null,
79 | },
80 | {
81 | name: 'Responsive',
82 | description: 'Responsive web design using many different devices.',
83 | link: 'https://www.w3schools.com/css/css_rwd_intro.asp',
84 | github: null,
85 | docs: null,
86 | },
87 | {
88 | name: 'ESLint',
89 | description: 'A utility-first CSS framework for rapid UI development.',
90 | link: 'https://eslint.org/',
91 | github: 'https://github.com/eslint/eslint',
92 | docs: 'https://eslint.org/docs/user-guide/getting-started',
93 | },
94 | {
95 | name: 'Prettier',
96 | description: 'An opinionated code formatter.',
97 | link: 'https://prettier.io/',
98 | github: 'https://github.com/prettier/prettier',
99 | docs: 'https://prettier.io/docs/en/index.html',
100 | },
101 | {
102 | name: 'Lint Staged',
103 | description: 'Run linters on git staged files.',
104 | link: null,
105 | github: 'https://github.com/okonet/lint-staged',
106 | docs: null,
107 | },
108 | {
109 | name: 'Conventional Commits',
110 | description:
111 | 'A specification for adding human and machine readable meaning to commit messages.',
112 | link: 'https://www.conventionalcommits.org/',
113 | github: null,
114 | docs: null,
115 | },
116 | {
117 | name: 'Husky',
118 | description: 'Modern native git hooks made easy.',
119 | link: 'https://typicode.github.io/husky/',
120 | github: 'https://github.com/typicode/husky',
121 | docs: null,
122 | },
123 | {
124 | name: 'Release It',
125 | description: 'Automate versioning and package publishing',
126 | link: null,
127 | github: 'https://github.com/release-it/release-it',
128 | docs: null,
129 | },
130 | {
131 | name: 'Typescript',
132 | description:
133 | 'Superior developer experience, code completion, refactoring and less bugs.',
134 | link: 'https://www.typescriptlang.org/',
135 | github: 'https://github.com/Microsoft/TypeScript',
136 | docs: 'https://www.typescriptlang.org/docs/',
137 | },
138 | ];
139 |
--------------------------------------------------------------------------------
/src/app/features/home/home.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 | Angular
10 | Authentication
11 |
12 |
13 | An
14 | Angular
15 | application that demonstrates best practices for user
16 | authentication
17 | &
18 | authorization
19 | flows.
20 |
21 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Features
45 |
46 | This repository uses state-of-the-art features for web development.
47 |
48 |
49 |
50 | @for (feature of features(); track $index) {
51 |
52 |
53 | {{ feature.name }}
54 |
55 |
56 |
57 | {{ feature.description }}
58 |
59 |
60 |
61 | @if (feature.link) {
62 |
70 |
71 |
72 | }
73 |
74 | @if (feature.github) {
75 |
83 |
84 |
85 | }
86 |
87 | @if (feature.docs) {
88 |
96 |
97 |
98 | }
99 |
100 |
101 | }
102 |
103 |
104 |
105 |
106 | {{ showFeaturesLabel() }}
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/src/app/features/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed, signal } from '@angular/core';
2 | import { MatButtonModule } from '@angular/material/button';
3 | import { MatCardModule } from '@angular/material/card';
4 | import { MatTooltipModule } from '@angular/material/tooltip';
5 |
6 | import { IconModule } from '../../shared/ui/icon';
7 |
8 | import { Feature, features } from './features.data';
9 |
10 | @Component({
11 | selector: 'aa-home',
12 | imports: [IconModule, MatButtonModule, MatCardModule, MatTooltipModule],
13 | templateUrl: './home.component.html',
14 | })
15 | export class HomeComponent {
16 | readonly showAllFeatures = signal(false);
17 |
18 | readonly showFeaturesLabel = computed(() =>
19 | this.showAllFeatures() ? 'Show less' : 'Show more'
20 | );
21 | readonly features = computed(() =>
22 | this.showAllFeatures() ? features : features.slice(0, 8)
23 | );
24 |
25 | toggleFeatures() {
26 | this.showAllFeatures.update(show => !show);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/features/home/index.ts:
--------------------------------------------------------------------------------
1 | export * from './home.component';
2 |
--------------------------------------------------------------------------------
/src/app/features/secured-feat/index.ts:
--------------------------------------------------------------------------------
1 | export * from './secured-feat.component';
2 |
--------------------------------------------------------------------------------
/src/app/features/secured-feat/secured-feat.component.html:
--------------------------------------------------------------------------------
1 | @if (vm$ | async; as vm) {
2 |
3 |
4 | {{ currentTime() | greeting }},
5 |
6 | {{ vm.authUser?.firstName }} {{ vm.authUser?.lastName }}
7 |
8 |
9 |
10 |
11 | Shhh... 🤫 - This is a
12 | top secret
13 | feature!
14 |
15 |
16 |
17 |
18 | Users
19 |
20 |
21 |
22 |
23 | Note that we expose users' credentials for
24 | demonstration purposes
25 | only! Feel free to logout and login again using one of the following accounts.
26 |
27 |
28 |
29 |
30 |
31 | Id
32 | {{ user.id }}
33 |
34 |
35 |
36 |
37 | Full Name
38 |
39 | {{ user.firstName }} {{ user.lastName }}
40 |
41 |
42 |
43 |
44 |
45 | Username
46 | {{ user.username }}
47 |
48 |
49 |
50 |
51 | Password
52 | {{ user.password }}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/features/secured-feat/secured-feat.component.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe } from '@angular/common';
2 | import { Component, inject, signal } from '@angular/core';
3 | import { MatCardModule } from '@angular/material/card';
4 | import { MatTableModule } from '@angular/material/table';
5 | import { combineLatest, of } from 'rxjs';
6 |
7 | import { AUTH_FACADE } from '../../auth';
8 | import { USERS } from '../../core/fake-api';
9 | import { GreetingPipe } from '../../shared/pipes';
10 | @Component({
11 | selector: 'aa-secured-feat',
12 | imports: [AsyncPipe, MatCardModule, MatTableModule, GreetingPipe],
13 | templateUrl: './secured-feat.component.html',
14 | })
15 | export class SecuredFeatComponent {
16 | private readonly authFacade = inject(AUTH_FACADE);
17 |
18 | readonly displayedColumns: string[] = ['id', 'name', 'username', 'password'];
19 | readonly currentTime = signal(new Date());
20 |
21 | readonly vm$ = combineLatest({
22 | authUser: this.authFacade.authUser$,
23 | users: of(USERS),
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/greeting.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'greeting',
5 | })
6 | export class GreetingPipe implements PipeTransform {
7 | transform(date: Date = new Date()): string {
8 | const hour = date.getHours();
9 | if (hour < 12) return 'Good morning';
10 | if (hour < 18) return 'Good afternoon';
11 | return 'Good evening';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/index.ts:
--------------------------------------------------------------------------------
1 | export * from './greeting.pipe';
2 |
--------------------------------------------------------------------------------
/src/app/shared/ui/avatar/avatar.component.scss:
--------------------------------------------------------------------------------
1 | $size: 2.5rem;
2 | $radius: 0.75rem;
3 |
4 | :host {
5 | position: relative;
6 | display: flex;
7 | flex-shrink: 0;
8 | text-align: center;
9 | text-transform: uppercase;
10 | justify-content: center;
11 | align-items: center;
12 | user-select: none;
13 | overflow: hidden;
14 | font-weight: 500;
15 | border-radius: $radius;
16 | width: $size;
17 | height: $size;
18 |
19 | @apply aa--bg-secondary aa--text-primary;
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/shared/ui/avatar/avatar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'aa-avatar',
5 | template: `
6 | {{ computedText }}
7 | `,
8 | styleUrl: './avatar.component.scss',
9 | })
10 | export class AvatarComponent {
11 | @Input({ required: true })
12 | text = '';
13 |
14 | get computedText(): string {
15 | if (!this.text) return '';
16 |
17 | const words = this.text.split(' ');
18 |
19 | return words.length > 1
20 | ? words[0].slice(0, 1) + words[1].slice(0, 1)
21 | : words[0].slice(0, 1);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/shared/ui/avatar/index.ts:
--------------------------------------------------------------------------------
1 | export * from './avatar.component';
2 |
--------------------------------------------------------------------------------
/src/app/shared/ui/footer/footer.component.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/app/shared/ui/footer/footer.component.ts:
--------------------------------------------------------------------------------
1 | import { DatePipe } from '@angular/common';
2 | import { Component, Input } from '@angular/core';
3 | import { MatButtonModule } from '@angular/material/button';
4 | import { IconProp } from '@fortawesome/fontawesome-svg-core';
5 |
6 | import { IconModule } from '../icon';
7 |
8 | interface PersonalLink {
9 | label: string;
10 | href: string;
11 | icon: IconProp;
12 | }
13 |
14 | @Component({
15 | selector: 'aa-footer',
16 | imports: [DatePipe, IconModule, MatButtonModule],
17 | templateUrl: './footer.component.html',
18 | })
19 | export class FooterComponent {
20 | @Input({ required: true })
21 | version = '';
22 |
23 | readonly now = new Date();
24 | readonly personalLinks: PersonalLink[] = [
25 | {
26 | label: 'GitHub',
27 | href: 'https://github.com/nikosanif',
28 | icon: ['fab', 'github'],
29 | },
30 | {
31 | label: 'Medium',
32 | href: 'https://nikosanif.medium.com/',
33 | icon: ['fab', 'medium-m'],
34 | },
35 | {
36 | label: 'LinkedIn',
37 | href: 'https://www.linkedin.com/in/nikosanifantis/',
38 | icon: ['fab', 'linkedin-in'],
39 | },
40 | {
41 | label: 'X',
42 | href: 'https://x.com/nikosanif',
43 | icon: ['fab', 'x-twitter'],
44 | },
45 | ];
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/shared/ui/footer/index.ts:
--------------------------------------------------------------------------------
1 | export * from './footer.component';
2 |
--------------------------------------------------------------------------------
/src/app/shared/ui/header/header.component.html:
--------------------------------------------------------------------------------
1 |
68 |
--------------------------------------------------------------------------------
/src/app/shared/ui/header/header.component.scss:
--------------------------------------------------------------------------------
1 | header {
2 | box-shadow: 0 0.125rem 1rem rgb(0 0 0 / 8%);
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | padding: 0 1.25rem;
7 | border-bottom: 1px solid #ededed;
8 | height: 4rem;
9 | @apply aa--bg-white;
10 | }
11 |
12 | .logo-icon {
13 | height: 50px;
14 | margin-right: 5px;
15 | }
16 |
17 | .menu-items {
18 | border-left: 1px solid #dedede;
19 | margin-left: 24px;
20 | padding-left: 24px;
21 |
22 | > a {
23 | @apply aa--mx-1;
24 |
25 | &.active {
26 | @apply aa--bg-secondary;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/shared/ui/header/header.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, output } from '@angular/core';
2 | import { MatButtonModule } from '@angular/material/button';
3 | import { MatMenuModule } from '@angular/material/menu';
4 | import { MatTooltipModule } from '@angular/material/tooltip';
5 | import { RouterLink, RouterLinkActive } from '@angular/router';
6 | import { IconProp } from '@fortawesome/fontawesome-svg-core';
7 |
8 | import { AuthUser } from '../../../auth';
9 | import { AvatarComponent } from '../avatar';
10 | import { IconModule } from '../icon';
11 |
12 | interface MenuItem {
13 | link: string;
14 | label: string;
15 | icon: IconProp;
16 | }
17 |
18 | @Component({
19 | selector: 'aa-header',
20 | imports: [
21 | AvatarComponent,
22 | IconModule,
23 | MatButtonModule,
24 | MatMenuModule,
25 | MatTooltipModule,
26 | RouterLink,
27 | RouterLinkActive,
28 | ],
29 | templateUrl: './header.component.html',
30 | styleUrls: ['./header.component.scss'],
31 | })
32 | export class HeaderComponent {
33 | @Input({ required: true })
34 | authUser: AuthUser | null | undefined = null;
35 |
36 | readonly logout = output();
37 |
38 | readonly menuItems: MenuItem[] = [
39 | { link: '/home', label: 'Home', icon: 'home' },
40 | { link: '/about', label: 'About', icon: 'info-circle' },
41 | { link: '/secured-feat', label: 'Secured Feature', icon: 'lock' },
42 | ];
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/shared/ui/header/index.ts:
--------------------------------------------------------------------------------
1 | export * from './header.component';
2 |
--------------------------------------------------------------------------------
/src/app/shared/ui/icon/icon.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, inject } from '@angular/core';
2 | import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
3 | import {
4 | faGithub,
5 | faMediumM,
6 | faXTwitter,
7 | faLinkedinIn,
8 | } from '@fortawesome/free-brands-svg-icons';
9 | import {
10 | faStar,
11 | faBook,
12 | faLink,
13 | faLock,
14 | faUser,
15 | faRightFromBracket,
16 | faHome,
17 | faInfoCircle,
18 | } from '@fortawesome/free-solid-svg-icons';
19 |
20 | @NgModule({
21 | imports: [FontAwesomeModule],
22 | exports: [FontAwesomeModule],
23 | })
24 | export class IconModule {
25 | private readonly faIconLibrary = inject(FaIconLibrary);
26 |
27 | private icons = [
28 | faGithub,
29 | faMediumM,
30 | faXTwitter,
31 | faLinkedinIn,
32 | faStar,
33 | faBook,
34 | faLink,
35 | faLock,
36 | faUser,
37 | faRightFromBracket,
38 | faHome,
39 | faInfoCircle,
40 | ];
41 |
42 | constructor() {
43 | this.faIconLibrary.addIcons(...this.icons);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/shared/ui/icon/index.ts:
--------------------------------------------------------------------------------
1 | export * from './icon.module';
2 |
--------------------------------------------------------------------------------
/src/environments/environment.development.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-require-imports
2 | const { version } = require('../../package.json');
3 |
4 | export const environment = {
5 | production: false,
6 | appVersion: `${version}-dev`,
7 |
8 | // Replace this with your server API URL
9 | // We assigned it to empty string for the Fake API
10 | apiUrl: '',
11 |
12 | settings: {
13 | auth: {
14 | // OAuth2 credentials
15 | clientId: 'fake-client-id', //
16 | secretId: 'fake-secret-id', //
17 |
18 | // keys to store tokens at local storage
19 | accessTokenKey: 'DoPS3ZrQjM',
20 | refreshTokenKey: 'nmlP8PW2nb',
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-require-imports
2 | const { version } = require('../../package.json');
3 |
4 | export const environment = {
5 | production: true,
6 | appVersion: version,
7 |
8 | // Replace this with your server API URL
9 | // We assigned it to empty string for the Fake API
10 | apiUrl: '',
11 |
12 | settings: {
13 | auth: {
14 | // OAuth2 credentials
15 | clientId: 'fake-client-id', //
16 | secretId: 'fake-secret-id', //
17 |
18 | // keys to store tokens at local storage
19 | accessTokenKey: 'DoPS3ZrQjM',
20 | refreshTokenKey: 'nmlP8PW2nb',
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularAuthentication
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
43 |
47 |
48 |
49 |
53 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { bootstrapApplication } from '@angular/platform-browser';
3 |
4 | import { AppComponent } from './app/app.component';
5 | import { appConfig } from './app/app.config';
6 | import { environment } from './environments/environment';
7 |
8 | if (environment.production) {
9 | enableProdMode();
10 | }
11 |
12 | bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | // Import tailwindcss styles
2 | @use 'tailwindcss/base';
3 | @use 'tailwindcss/components';
4 | @use 'tailwindcss/utilities';
5 |
6 | @use './theme';
7 |
8 | html,
9 | body {
10 | height: 100%;
11 | }
12 | body {
13 | margin: 0;
14 | font-family:
15 | ui-sans-serif,
16 | system-ui,
17 | -apple-system,
18 | BlinkMacSystemFont,
19 | Segoe UI,
20 | Roboto,
21 | Helvetica Neue,
22 | Arial,
23 | Noto Sans,
24 | sans-serif,
25 | Apple Color Emoji,
26 | Segoe UI Emoji,
27 | Segoe UI Symbol,
28 | Noto Color Emoji;
29 | }
30 |
--------------------------------------------------------------------------------
/src/theme/_components.scss:
--------------------------------------------------------------------------------
1 | // Section styles
2 | section {
3 | @apply aa--mb-12 aa--mt-4 lg:aa--mb-16;
4 | }
5 |
6 | // Alert styles
7 | .alert {
8 | @apply aa--mb-4 aa--rounded-lg aa--p-4;
9 |
10 | &.alert--success {
11 | @apply aa--bg-green-100 aa--text-green-800;
12 | }
13 |
14 | &.alert--error {
15 | @apply aa--bg-red-100 aa--text-red-800;
16 | }
17 |
18 | &.alert--info {
19 | @apply aa--bg-blue-100 aa--text-blue-800;
20 | }
21 |
22 | &.alert--warning {
23 | @apply aa--bg-yellow-100 aa--text-yellow-800;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/theme/_material.scss:
--------------------------------------------------------------------------------
1 | // Custom Theming for Angular Material
2 | // For more information: https://material.angular.io/guide/theming
3 | @use '@angular/material' as mat;
4 | // Plus imports for other components in your app.
5 |
6 | // Include the common styles for Angular Material. We include this here so that you only
7 | // have to load a single css file for Angular Material in your app.
8 | // Be sure that you only ever include this mixin once!
9 | @include mat.core();
10 |
11 | $font-main:
12 | ui-sans-serif,
13 | system-ui,
14 | -apple-system,
15 | BlinkMacSystemFont,
16 | Segoe UI,
17 | Roboto,
18 | Helvetica Neue,
19 | Arial,
20 | Noto Sans,
21 | sans-serif,
22 | Apple Color Emoji,
23 | Segoe UI Emoji,
24 | Segoe UI Symbol,
25 | Noto Color Emoji;
26 |
27 | // Define the theme object.
28 | $angular-authentication-theme: mat.define-theme(
29 | (
30 | color: (
31 | theme-type: light,
32 | primary: mat.$violet-palette,
33 | tertiary: mat.$cyan-palette,
34 | ),
35 | typography: (
36 | plain-family: $font-main,
37 | brand-family: $font-main,
38 | ),
39 | density: (
40 | scale: 0,
41 | ),
42 | )
43 | );
44 |
45 | // Include theme styles for core and each component used in your app.
46 | // Alternatively, you can import and @include the theme mixins for each component
47 | // that you are using.
48 | :root {
49 | @include mat.all-component-themes($angular-authentication-theme);
50 | }
51 |
52 | // Override material variables
53 | :root {
54 | --mdc-elevated-card-container-color: #fff;
55 | --mat-table-background-color: #fff;
56 | }
57 |
58 | // Comment out the line below if you want to use the pre-defined typography utility classes.
59 | // For more information: https://material.angular.io/guide/typography#using-typography-styles-in-your-application.
60 | @include mat.typography-hierarchy($angular-authentication-theme);
61 |
--------------------------------------------------------------------------------
/src/theme/index.scss:
--------------------------------------------------------------------------------
1 | @use './material.scss';
2 | @use './components.scss';
3 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | prefix: 'aa--',
3 | important: true,
4 | content: ['./src/**/*.{html,ts}'],
5 | theme: {
6 | extend: {
7 | container: {
8 | center: true,
9 | },
10 | colors: {
11 | primary: '#7d01fa',
12 | secondary: '#e8e1f7',
13 | accent: '#0dbfa9',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | };
19 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": ["node"]
7 | },
8 | "files": ["src/main.ts"],
9 | "include": ["src/**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "strict": true,
5 | "incremental": true
6 | },
7 | "exclude": ["**/node_modules", "*.js"]
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "outDir": "./dist/out-tsc",
6 | "strict": true,
7 | "noImplicitOverride": true,
8 | "noPropertyAccessFromIndexSignature": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "esModuleInterop": true,
12 | "sourceMap": true,
13 | "declaration": false,
14 | "experimentalDecorators": true,
15 | "isolatedModules": true,
16 | "moduleResolution": "bundler",
17 | "importHelpers": true,
18 | "target": "ES2022",
19 | "module": "ES2022",
20 | "useDefineForClassFields": false,
21 | "lib": ["ES2022", "dom"],
22 | "strictPropertyInitialization": true,
23 | "strictNullChecks": true,
24 | "strictBindCallApply": true,
25 | "strictFunctionTypes": true
26 | },
27 | "angularCompilerOptions": {
28 | "enableI18nLegacyMessageIdFormat": false,
29 | "strictInjectionParameters": true,
30 | "strictInputAccessModifiers": true,
31 | "strictTemplates": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------