├── src ├── assets │ ├── .gitkeep │ ├── .gitignore │ ├── config │ │ ├── .gitignore │ │ ├── config.dev.json │ │ └── config.prod.json │ └── i18n │ │ ├── en.json │ │ └── fi.json ├── app │ ├── app.component.scss │ ├── landing │ │ ├── landing.component.scss │ │ ├── landing.component.html │ │ └── landing.component.ts │ ├── auth │ │ ├── enums │ │ │ ├── index.ts │ │ │ └── role.enum.ts │ │ ├── factories │ │ │ ├── index.ts │ │ │ └── jwt-options.factory.ts │ │ ├── interfaces │ │ │ ├── credentials-response.interface.ts │ │ │ ├── role.interface.ts │ │ │ ├── credentials-request.interface.ts │ │ │ ├── user-data-value.interface.ts │ │ │ ├── user-profile-value.interface.ts │ │ │ ├── user-group.interface.ts │ │ │ ├── user-data.interface.ts │ │ │ ├── auth-guard-meta-data.interface.ts │ │ │ ├── user-profile.interface.ts │ │ │ ├── index.ts │ │ │ └── role-guard-meta-data.interface.ts │ │ ├── services │ │ │ └── index.ts │ │ ├── login │ │ │ ├── login.component.scss │ │ │ ├── login.routes.ts │ │ │ └── login.component.html │ │ ├── auth.routes.ts │ │ └── guards │ │ │ ├── index.ts │ │ │ ├── role-root.guard.ts │ │ │ ├── role-user.guard.ts │ │ │ ├── role-admin.guard.ts │ │ │ ├── role-logged.guard.ts │ │ │ ├── anonymous.guard.ts │ │ │ ├── authentication.guard.ts │ │ │ ├── base-role.ts │ │ │ └── base-auth.ts │ ├── shared │ │ ├── utils │ │ │ ├── index.ts │ │ │ └── ngrx.utils.ts │ │ ├── components │ │ │ ├── version-change-dialog │ │ │ │ ├── version-change-dialog.component.scss │ │ │ │ ├── version-content.interface.ts │ │ │ │ ├── version-change-dialog.component.html │ │ │ │ └── version-change-dialog.component.ts │ │ │ ├── oops │ │ │ │ ├── oops.component.scss │ │ │ │ ├── oops.component.ts │ │ │ │ └── oops.component.html │ │ │ ├── header │ │ │ │ ├── header.component.scss │ │ │ │ └── header.component.html │ │ │ ├── footer │ │ │ │ ├── footer.component.scss │ │ │ │ └── footer.component.html │ │ │ ├── error-message │ │ │ │ ├── error-message.component.scss │ │ │ │ └── error-message.component.html │ │ │ └── index.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── http-cache.type.ts │ │ │ └── intl.d.ts │ │ ├── Loaders │ │ │ ├── index.ts │ │ │ └── transloco-http.loader.ts │ │ ├── factories │ │ │ ├── index.ts │ │ │ └── http-loader.factory.ts │ │ ├── interfaces │ │ │ ├── dictionary.interface.ts │ │ │ ├── theme-value.interface.ts │ │ │ ├── locale-value.interface.ts │ │ │ ├── language-value.interface.ts │ │ │ ├── viewport-value.interface.ts │ │ │ ├── server-error-value.interface.ts │ │ │ ├── version.interface.ts │ │ │ ├── environment.interface.ts │ │ │ ├── application-configuration-interface.ts │ │ │ ├── server-error-debug.interface.ts │ │ │ ├── error-message.interface.ts │ │ │ ├── error-message-server.interface.ts │ │ │ ├── server-error.interface.ts │ │ │ ├── localization.interface.ts │ │ │ ├── error-message-client.interface.ts │ │ │ └── index.ts │ │ ├── enums │ │ │ ├── device.enum.ts │ │ │ ├── viewport.enum.ts │ │ │ ├── locale.enum.ts │ │ │ ├── language.enum.ts │ │ │ ├── theme.enum.ts │ │ │ └── index.ts │ │ ├── constants │ │ │ ├── index.ts │ │ │ ├── languages.constant.ts │ │ │ └── viewports.constant.ts │ │ ├── pipes │ │ │ ├── index.ts │ │ │ ├── local-number.pipe.ts │ │ │ └── local-date.pipe.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── http-cache.service.ts │ │ │ ├── version.service.ts │ │ │ └── configuration-service.ts │ │ ├── directives │ │ │ ├── auto-focus.directive.ts │ │ │ ├── index.ts │ │ │ ├── has-role.directive.ts │ │ │ ├── has-not-role.directive.ts │ │ │ ├── has-all-roles.directive.ts │ │ │ ├── has-not-all-roles.directive.ts │ │ │ ├── is-logged-in.directive.ts │ │ │ ├── has-some-role.directive.ts │ │ │ └── has-not-some-role.directive.ts │ │ └── interceptors │ │ │ ├── index.ts │ │ │ ├── accept-language.interceptor.ts │ │ │ ├── http-cache.interceptor.ts │ │ │ └── backend-version.interceptor.ts │ ├── store │ │ ├── meta-reducers │ │ │ ├── index.ts │ │ │ └── local-storage-sync.reducer.ts │ │ ├── aware-states │ │ │ ├── is-loading-aware.state.ts │ │ │ ├── index.ts │ │ │ └── server-error-aware.state.ts │ │ ├── error │ │ │ ├── error.type.ts │ │ │ ├── error.state.ts │ │ │ ├── error.reducer.ts │ │ │ ├── error.effects.ts │ │ │ └── error.actions.ts │ │ ├── store.reducers.ts │ │ ├── store.actions.ts │ │ ├── store.states.ts │ │ ├── store.types.ts │ │ ├── app.state.ts │ │ ├── store.selectors.ts │ │ ├── index.ts │ │ ├── app.effects.ts │ │ ├── version │ │ │ ├── version.state.ts │ │ │ ├── version.type.ts │ │ │ ├── version.selectors.ts │ │ │ ├── version.actions.ts │ │ │ └── version.reducer.ts │ │ ├── layout │ │ │ ├── layout.type.ts │ │ │ ├── layout.state.ts │ │ │ ├── layout.selectors.ts │ │ │ └── layout.actions.ts │ │ ├── app.reducers.ts │ │ ├── authentication │ │ │ ├── authentication.state.ts │ │ │ ├── authentication.type.ts │ │ │ ├── authentication.actions.ts │ │ │ └── authentication.reducer.ts │ │ └── router │ │ │ ├── router.selectors.ts │ │ │ └── router.effects.ts │ ├── app.component.html │ ├── app.locales.ts │ └── app.routes.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ ├── environment.ts │ └── environment.local-prod.ts ├── styles │ ├── layout.scss │ ├── animations.scss │ ├── variables.scss │ ├── mixins.scss │ └── palettes.scss ├── test.ts ├── polyfills.ts └── index.html ├── docker ├── fish │ ├── functions │ │ └── .gitignore │ ├── config.fish │ └── completions │ │ └── ng.fish ├── ssl │ ├── openssl.cnf │ ├── create-keys.sh │ ├── tls.csr │ ├── rootCA.pem │ ├── README.md │ ├── tls.key │ ├── rootCA.key │ └── tls.crt └── nginx.conf ├── markdown-lint.yml ├── .idea ├── watcherTasks.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── jsLibraryMappings.xml ├── misc.xml ├── modules.xml ├── runConfigurations │ └── Node.xml ├── angular-frontend.iml ├── vcs.xml └── copilot.data.migration.agent.xml ├── tsconfig.app.json ├── e2e ├── tsconfig.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── .editorconfig ├── tsconfig.spec.json ├── transloco.config.ts ├── compose.yaml ├── docker-sync.yml ├── docker-entrypoint-dev.sh ├── .browserslistrc ├── scripts ├── README.md └── project-stats.sh ├── doc ├── README.md ├── TOOLS.md ├── USAGE_CHECKLIST.md ├── DEPENDENCY_UPDATE.md ├── SPEED_UP_DOCKER_COMPOSE.md └── COMMANDS.md ├── tsconfig.json ├── .github ├── dependabot.yml └── workflows │ ├── vulnerability-scan.yml │ ├── codeql-analysis.yml │ └── scorecard.yml ├── version.js ├── .gitignore ├── LICENSE ├── karma.conf.js └── .stylelintrc.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/fish/functions/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/landing/landing.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/.gitignore: -------------------------------------------------------------------------------- 1 | version.json 2 | -------------------------------------------------------------------------------- /docker/fish/config.fish: -------------------------------------------------------------------------------- 1 | thefuck --alias | source 2 | -------------------------------------------------------------------------------- /src/assets/config/.gitignore: -------------------------------------------------------------------------------- 1 | config.*.local.json 2 | -------------------------------------------------------------------------------- /markdown-lint.yml: -------------------------------------------------------------------------------- 1 | MD026: 2 | punctuation: '.,;:!' 3 | -------------------------------------------------------------------------------- /src/app/auth/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/auth/enums/role.enum'; 2 | -------------------------------------------------------------------------------- /src/app/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/shared/utils/ngrx.utils'; 2 | -------------------------------------------------------------------------------- /src/app/shared/components/version-change-dialog/version-change-dialog.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/shared/types/http-cache.type'; 2 | -------------------------------------------------------------------------------- /src/app/auth/factories/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/auth/factories/jwt-options.factory'; 2 | -------------------------------------------------------------------------------- /src/app/shared/Loaders/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/shared/Loaders/transloco-http.loader'; 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-ngrx-frontend/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/app/shared/factories/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/shared/factories/http-loader.factory'; 2 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/dictionary.interface.ts: -------------------------------------------------------------------------------- 1 | export type DictionaryInterface = Record; 2 | -------------------------------------------------------------------------------- /src/app/store/meta-reducers/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/store/meta-reducers/local-storage-sync.reducer'; 2 | -------------------------------------------------------------------------------- /src/app/shared/components/oops/oops.component.scss: -------------------------------------------------------------------------------- 1 | .container-foo { 2 | width: 100vw; 3 | max-width: 320px; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/store/aware-states/is-loading-aware.state.ts: -------------------------------------------------------------------------------- 1 | export interface IsLoadingAwareState { 2 | isLoading: boolean; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/enums/device.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Device { 2 | DESKTOP = 'desktop', 3 | TABLET = 'tablet', 4 | MOBILE = 'mobile', 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/config/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "https://localhost:8000", 3 | "tokenUrl": "https://localhost:8000/v1/auth/get_token" 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/config/config.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "https://api.somedomain.tld", 3 | "tokenUrl": "https://api.somedomain.tld/token-url" 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/enums/viewport.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Viewport { 2 | XS = 'xs', 3 | SM = 'sm', 4 | MD = 'md', 5 | LG = 'lg', 6 | XL = 'xl', 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shared/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/shared/constants/languages.constant'; 2 | export * from 'src/app/shared/constants/viewports.constant'; 3 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/theme-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from 'src/app/shared/enums'; 2 | 3 | export interface ThemeValueInterface { 4 | theme: Theme; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/components/version-change-dialog/version-content.interface.ts: -------------------------------------------------------------------------------- 1 | export interface VersionContentInterface { 2 | versionOld: string; 3 | versionNew: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/locale-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from 'src/app/shared/enums'; 2 | 3 | export interface LocaleValueInterface { 4 | locale: Locale; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/store/aware-states/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/store/aware-states/is-loading-aware.state'; 2 | export * from 'src/app/store/aware-states/server-error-aware.state'; 3 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/language-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { Language } from 'src/app/shared/enums'; 2 | 3 | export interface LanguageValueInterface { 4 | language: Language; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/viewport-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { Viewport } from 'src/app/shared/enums'; 2 | 3 | export interface ViewportValueInterface { 4 | viewport: Viewport; 5 | } 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/credentials-response.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface to present credential response. 3 | */ 4 | export interface CredentialsResponseInterface { 5 | token: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.scss: -------------------------------------------------------------------------------- 1 | .header-container { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | background-color: #fff; 7 | z-index: 2; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/store/error/error.type.ts: -------------------------------------------------------------------------------- 1 | // Error store action definitions. 2 | export enum ErrorType { 3 | SHOW_SNACKBAR = '[Error] Show snackbar', 4 | CLEAR_SNACKBAR = '[Error] Clear snackbar', 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/constants/languages.constant.ts: -------------------------------------------------------------------------------- 1 | import { Language } from 'src/app/shared/enums'; 2 | 3 | export const languages: Array = [ 4 | Language.ENGLISH, 5 | Language.FINNISH, 6 | ]; 7 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/role.interface.ts: -------------------------------------------------------------------------------- 1 | import { Role } from 'src/app/auth/enums'; 2 | 3 | /** 4 | * Interface to present `Role` object. 5 | */ 6 | export interface RoleInterface { 7 | id: Role; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/shared/enums/locale.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Locale { 2 | DEFAULT = 'en', 3 | // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values 4 | ENGLISH = 'en', 5 | FINNISH = 'fi', 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/enums/language.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Language { 2 | DEFAULT = 'en', 3 | // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values 4 | ENGLISH = 'en', 5 | FINNISH = 'fi', 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/types/http-cache.type.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@angular/common/http'; 2 | 3 | export type HttpCacheType = Record, 5 | timestamp: number, 6 | }>; 7 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/credentials-request.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface to present login request. 3 | */ 4 | export interface CredentialsRequestInterface { 5 | username: string; 6 | password: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/store/aware-states/server-error-aware.state.ts: -------------------------------------------------------------------------------- 1 | import { ServerErrorInterface } from 'src/app/shared/interfaces'; 2 | 3 | export interface ServerErrorAwareState { 4 | error: ServerErrorInterface|null; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/enums/theme.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Theme { 2 | DEFAULT = 'dark-theme', 3 | // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values 4 | DARK = 'dark-theme', 5 | LIGHT = 'light-theme', 6 | } 7 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/user-data-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserDataInterface } from 'src/app/auth/interfaces/user-data.interface'; 2 | 3 | export interface UserDataValueInterface { 4 | userData: UserDataInterface; 5 | } 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/user-profile-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserProfileInterface } from 'src/app/auth/interfaces/user-profile.interface'; 2 | 3 | export interface UserProfileValueInterface { 4 | profile: UserProfileInterface; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/server-error-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { ServerErrorInterface } from 'src/app/shared/interfaces/server-error.interface'; 2 | 3 | export interface ServerErrorValueInterface { 4 | error: ServerErrorInterface; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/auth/services/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationService } from 'src/app/auth/services/authentication.service'; 2 | 3 | export * from 'src/app/auth/services/authentication.service'; 4 | 5 | export const authenticationServices = [ 6 | AuthenticationService, 7 | ]; 8 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/version.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface definition for front- and backend version information. 3 | * 4 | * version 5 | * Version number as in semver format. 6 | */ 7 | export interface VersionInterface { 8 | version: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.scss: -------------------------------------------------------------------------------- 1 | @use '../../../styles/variables' as vars; 2 | 3 | .login-container { 4 | width: 360px; 5 | padding: vars.$spacing; 6 | background-color: var(--theme-background-component); 7 | 8 | h1 { 9 | text-align: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/types/intl.d.ts: -------------------------------------------------------------------------------- 1 | // See https://github.com/microsoft/TypeScript/issues/49231 2 | declare namespace Intl { 3 | type Key = 'calendar' | 'collation' | 'currency' | 'numberingSystem' | 'timeZone' | 'unit'; 4 | 5 | function supportedValuesOf(input: Key): Array; 6 | } 7 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import packageInfo from 'package.json'; 2 | import { EnvironmentInterface } from 'src/app/shared/interfaces'; 3 | 4 | export const environment: EnvironmentInterface = { 5 | production: true, 6 | name: 'prod', 7 | version: packageInfo.version, 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/shared/enums/device.enum'; 2 | export * from 'src/app/shared/enums/language.enum'; 3 | export * from 'src/app/shared/enums/locale.enum'; 4 | export * from 'src/app/shared/enums/theme.enum'; 5 | export * from 'src/app/shared/enums/viewport.enum'; 6 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/user-group.interface.ts: -------------------------------------------------------------------------------- 1 | import { RoleInterface } from 'src/app/auth/interfaces/role.interface'; 2 | 3 | /** 4 | * Interface to present `UserGroup` object. 5 | */ 6 | export interface UserGroupInterface { 7 | id: string; 8 | name: string; 9 | role: RoleInterface; 10 | } 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /src/app/store/store.reducers.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/store/authentication/authentication.reducer'; 2 | export * from 'src/app/store/error/error.reducer'; 3 | export * from 'src/app/store/layout/layout.reducer'; 4 | export * from 'src/app/store/meta-reducers'; 5 | export * from 'src/app/store/version/version.reducer'; 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/styles/layout.scss: -------------------------------------------------------------------------------- 1 | @use './variables' as vars; 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | font-family: vars.$font-family; 11 | } 12 | 13 | app-root { 14 | display: flex; 15 | position: absolute; 16 | inset: vars.$header-height 0 0 0; 17 | flex-direction: column; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/landing/landing.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | This is landing page of your application. 4 |

5 | 6 |

7 | You're currently using this application in {{ device$ | async }} mode and using viewport: {{ viewport$ | async }}. 8 |

9 |
10 | -------------------------------------------------------------------------------- /src/app/store/error/error.state.ts: -------------------------------------------------------------------------------- 1 | import { ServerErrorInterface } from 'src/app/shared/interfaces'; 2 | 3 | /** 4 | * Interface definition for our error store contents. 5 | * 6 | * errorSnackbar 7 | * Error object that is shown in snackbar. 8 | */ 9 | export interface ErrorState { 10 | errorSnackbar: ServerErrorInterface|null; 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [*.md] 16 | max_line_length = off 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /src/app/shared/constants/viewports.constant.ts: -------------------------------------------------------------------------------- 1 | import { Device, Viewport } from 'src/app/shared/enums'; 2 | 3 | export const viewports = { 4 | [Device.MOBILE]: [ 5 | Viewport.XS, 6 | Viewport.SM, 7 | ], 8 | [Device.TABLET]: [ 9 | Viewport.MD, 10 | ], 11 | [Device.DESKTOP]: [ 12 | Viewport.LG, 13 | Viewport.XL, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | public navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | public getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | .footer-container { 2 | .mat-toolbar-single-row { 3 | height: 32px; 4 | } 5 | 6 | .mat-mdc-button { 7 | border-radius: 0; 8 | font-size: 12px; 9 | line-height: 32px; 10 | } 11 | 12 | small { 13 | font-size: 10px; 14 | font-weight: 400; 15 | cursor: default; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/auth/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { loginRoutes } from 'src/app/auth/login/login.routes'; 4 | 5 | export const authRoutes: Routes = [ 6 | { 7 | path: '', 8 | children: [ 9 | ...loginRoutes, 10 | { 11 | path: '**', 12 | redirectTo: 'login', 13 | }, 14 | ], 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/user-data.interface.ts: -------------------------------------------------------------------------------- 1 | import { Role } from 'src/app/auth/enums'; 2 | import { LocalizationInterface } from 'src/app/shared/interfaces'; 3 | 4 | /** 5 | * Interface to present user data that is stored to Json Web Token (JWT). 6 | */ 7 | export interface UserDataInterface { 8 | roles: Array; 9 | localization: LocalizationInterface; 10 | } 11 | -------------------------------------------------------------------------------- /docker/ssl/openssl.cnf: -------------------------------------------------------------------------------- 1 | # Extensions to add to a certificate request 2 | basicConstraints = CA:FALSE 3 | authorityKeyIdentifier = keyid:always, issuer:always 4 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment 5 | subjectAltName = @alt_names 6 | 7 | [v3_req] 8 | subjectAltName = @alt_names 9 | 10 | [alt_names] 11 | DNS.1 = *.localhost 12 | DNS.2 = localhost 13 | -------------------------------------------------------------------------------- /docker/fish/completions/ng.fish: -------------------------------------------------------------------------------- 1 | # ng help 2 | complete -f -c ng -n '__fish_use_subcommand' -a help -d 'Outputs the usage instructions for all commands or the provided command.' 3 | 4 | # ng version 5 | complete -f -c ng -n '__fish_use_subcommand' -a version -d 'Outputs angular-cli version.' 6 | complete -f -A -c ng -n '__fish_seen_subcommand_from version' -l verbose -d 'verbose (Boolean) (Default: false)' 7 | -------------------------------------------------------------------------------- /.idea/angular-frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /transloco.config.ts: -------------------------------------------------------------------------------- 1 | import { TranslocoGlobalConfig } from '@jsverse/transloco-utils'; 2 | 3 | //import { languages } from './src/app/shared/constants'; 4 | 5 | const config: TranslocoGlobalConfig = { 6 | rootTranslationsPath: 'src/assets/i18n/', 7 | langs: ['fi', 'en'], 8 | keysManager: { 9 | addMissingKeys: true, 10 | //defaultValue: undefined, 11 | }, 12 | }; 13 | 14 | export default config; 15 | 16 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | node: 3 | container_name: angular-ngrx-frontend 4 | hostname: node 5 | build: 6 | context: . 7 | dockerfile: ./Dockerfile 8 | target: development 9 | command: sh -c 'if [ -z $PROD_MODE ]; then make start-yarn; else make start-yarn-prod; fi' 10 | user: $HOST_UID:$HOST_GID 11 | ports: 12 | - "4200:4200" 13 | volumes: 14 | - ./:/app:cached 15 | -------------------------------------------------------------------------------- /docker/ssl/create-keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | openssl genrsa -passout pass: -des3 -out rootCA.key 2048 4 | openssl req -x509 -new -nodes -passin pass: -key rootCA.key -sha256 -days 10000 -out rootCA.pem 5 | openssl genrsa -out tls.key 2048 6 | openssl req -new -key tls.key -out tls.csr 7 | openssl x509 -req -in tls.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out tls.crt -days 10000 -sha256 -passin pass: -extfile openssl.cnf 8 | -------------------------------------------------------------------------------- /docker-sync.yml: -------------------------------------------------------------------------------- 1 | # 2 | # For osx users, see http://docker-sync.io/ 3 | # this file should be added to your VCS 4 | # 5 | version: '2' 6 | 7 | syncs: 8 | frontend-code: 9 | src: ./ 10 | sync_args: 11 | - "-ignore='Path .idea'" # no need to send PHPStorm config to container 12 | - "-ignore='Path .git'" # ignore the main .git repo 13 | - "-ignore='BelowPath .git'" # also ignore .git repos in subfolders 14 | -------------------------------------------------------------------------------- /src/app/shared/pipes/index.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | 3 | import { LocalDatePipe } from 'src/app/shared/pipes/local-date.pipe'; 4 | import { LocalNumberPipe } from 'src/app/shared/pipes/local-number.pipe'; 5 | 6 | export * from 'src/app/shared/pipes/local-date.pipe'; 7 | export * from 'src/app/shared/pipes/local-number.pipe'; 8 | 9 | export const pipes: Array> = [ 10 | LocalDatePipe, 11 | LocalNumberPipe, 12 | ]; 13 | -------------------------------------------------------------------------------- /src/app/shared/services/index.ts: -------------------------------------------------------------------------------- 1 | import { SnackbarService } from 'src/app/shared/services/snackbar-service'; 2 | import { VersionService } from 'src/app/shared/services/version.service'; 3 | 4 | export * from 'src/app/shared/services/configuration-service'; 5 | export * from 'src/app/shared/services/snackbar-service'; 6 | export * from 'src/app/shared/services/version.service'; 7 | 8 | export const services = [ 9 | SnackbarService, 10 | VersionService, 11 | ]; 12 | -------------------------------------------------------------------------------- /docker-entrypoint-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # 5 | # This is default entrypoint for local development of this Angular application. 6 | # Within this we need to do following steps to build application correctly: 7 | # 1) Make sure that all dependencies are up-to-date 8 | # 2) Execute specified command, eg. with `make start` command this will be 9 | # `sh -c 'make start-yarn'` 10 | # 11 | 12 | # Step 1 13 | yarn install 14 | 15 | # Execute 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /src/app/store/store.actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Barrel file for all NgRx `actions` for this application. When you add new 3 | * one just add it here and it will be usable everywhere in this application 4 | * via that main NgRx store barrel file. 5 | */ 6 | 7 | export * from 'src/app/store/authentication/authentication.actions'; 8 | export * from 'src/app/store/error/error.actions'; 9 | export * from 'src/app/store/layout/layout.actions'; 10 | export * from 'src/app/store/version/version.actions'; 11 | -------------------------------------------------------------------------------- /src/app/store/store.states.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Barrel file for all NgRx `state` definitions for this application. When you 3 | * add new one just add it here and it will be usable everywhere in this 4 | * application via that main NgRx store barrel file. 5 | */ 6 | 7 | export * from 'src/app/store/authentication/authentication.state'; 8 | export * from 'src/app/store/error/error.state'; 9 | export * from 'src/app/store/layout/layout.state'; 10 | export * from 'src/app/store/version/version.state'; 11 | -------------------------------------------------------------------------------- /src/app/store/store.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Barrel file for all NgRx `action` type definitions for this application. 3 | * When you add new one just add it here and it will be usable everywhere in 4 | * this application via that main NgRx store barrel file. 5 | */ 6 | 7 | export * from 'src/app/store/authentication/authentication.type'; 8 | export * from 'src/app/store/error/error.type'; 9 | export * from 'src/app/store/layout/layout.type'; 10 | export * from 'src/app/store/version/version.type'; 11 | -------------------------------------------------------------------------------- /src/app/store/app.state.ts: -------------------------------------------------------------------------------- 1 | import { BaseRouterStoreState, RouterReducerState } from '@ngrx/router-store'; 2 | 3 | import { AuthenticationState, ErrorState, LayoutState, VersionState } from 'src/app/store/store.states'; 4 | 5 | /** 6 | * Application NgRx main state definition. 7 | */ 8 | export interface AppState { 9 | router: RouterReducerState; 10 | authentication: AuthenticationState; 11 | error: ErrorState; 12 | layout: LayoutState; 13 | version: VersionState; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/store/store.selectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Barrel file for all NgRx `selectors` definitions for this application. When 3 | * you add new one just add it here and it will be usable everywhere in this 4 | * application via that main NgRx store barrel file. 5 | */ 6 | 7 | export * from 'src/app/store/authentication/authentication.selectors'; 8 | export * from 'src/app/store/layout/layout.selectors'; 9 | export * from 'src/app/store/router/router.selectors'; 10 | export * from 'src/app/store/version/version.selectors'; 11 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
7 |
11 |
14 |
17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This directory contains different scripts that are used during development. 4 | 5 | * [What is this](#what-is-this) 6 | * [Table of Contents](#table-of-contents) 7 | * [Resources](#resources-table-of-contents) 8 | 9 | ## Resources [ᐞ](#table-of-contents) 10 | 11 | * [Project stats script](project-stats.sh) 12 | * This script is used to generate simple project stats. It will generate 13 | output with some basic stats about project. 14 | 15 | --- 16 | 17 | [Back to main README.md](../README.md) 18 | -------------------------------------------------------------------------------- /src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main barrel file for NgRx store main state, actions, selectors, sub states, 3 | * action type enums and combined type definitions. 4 | * 5 | * You should use this barrel file with import statements within this 6 | * application when you're using NgRx related stuff. 7 | */ 8 | 9 | export * from 'src/app/store/app.state'; 10 | export * from 'src/app/store/store.actions'; 11 | export * from 'src/app/store/store.selectors'; 12 | export * from 'src/app/store/store.states'; 13 | export * from 'src/app/store/store.types'; 14 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/auth-guard-meta-data.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for auth guard meta data, which is used on following guards within 3 | * this application; 4 | * - AnonymousGuard 5 | * - AuthenticationGuard 6 | * 7 | * With this you can change the guard behaviour by specified route `data` 8 | * property. For detailed information see `src/app/auth/guards/base-auth.ts` 9 | */ 10 | export interface AuthGuardMetaDataInterface { 11 | redirectIfMismatch: boolean; 12 | routeAuthenticated: string; 13 | routeNotAuthenticated: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.locales.ts: -------------------------------------------------------------------------------- 1 | import { registerLocaleData } from '@angular/common'; 2 | import localeEn from '@angular/common/locales/en'; 3 | import localeEnExtra from '@angular/common/locales/extra/en'; 4 | import localeFiExtra from '@angular/common/locales/extra/fi'; 5 | import localeFi from '@angular/common/locales/fi'; 6 | 7 | import { Locale } from 'src/app/shared/enums'; 8 | 9 | export const registerLocales = (): void => { 10 | registerLocaleData(localeEn, Locale.ENGLISH, localeEnExtra); 11 | registerLocaleData(localeFi, Locale.FINNISH, localeFiExtra); 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/shared/Loaders/transloco-http.loader.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { inject, Injectable } from '@angular/core'; 3 | import { Translation, TranslocoLoader } from '@jsverse/transloco'; 4 | 5 | import { Language } from 'src/app/shared/enums'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class TranslocoHttpLoader implements TranslocoLoader { 9 | private readonly http = inject(HttpClient); 10 | 11 | public getTranslation(lang: Language) { 12 | return this.http.get(`/assets/i18n/${lang}.json`); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserTestingModule, 6 | platformBrowserDynamicTesting, 7 | } from '@angular/platform-browser/testing'; 8 | import 'zone.js/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserTestingModule, 13 | platformBrowserDynamicTesting(), 14 | { 15 | teardown: { 16 | destroyAfterEach: false, 17 | }, 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/app/shared/components/error-message/error-message.component.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/variables' as vars; 2 | 3 | .mat-mdc-list-base[dense] { 4 | .mat-mdc-list-item { 5 | height: auto; 6 | 7 | ::ng-deep { 8 | /** 9 | * TODO(mdc-migration): The following rule targets internal classes of list that may no longer apply for 10 | * the MDC version. 11 | */ 12 | .mat-list-item-content { 13 | padding: 0 0 vars.$spacing-half; 14 | color: rgb(255, 255, 255, .7); 15 | line-height: 12px; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | 3 | export * from 'src/app/shared/components/error-message/error-message.component'; 4 | export * from 'src/app/shared/components/footer/footer.component'; 5 | export * from 'src/app/shared/components/header/header.component'; 6 | export * from 'src/app/shared/components/oops/oops.component'; 7 | export * from 'src/app/shared/components/version-change-dialog/version-change-dialog.component'; 8 | 9 | // Only export components that are used commonly within another modules 10 | export const components: Array> = [ 11 | ]; 12 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/user-profile.interface.ts: -------------------------------------------------------------------------------- 1 | import { Language, Locale } from 'src/app//shared/enums'; 2 | import { Role } from 'src/app/auth/enums'; 3 | import { UserGroupInterface } from 'src/app/auth/interfaces/user-group.interface'; 4 | 5 | /** 6 | * Interface to present user profile. 7 | */ 8 | export interface UserProfileInterface { 9 | id: string; 10 | username: string; 11 | firstName: string; 12 | lastName: string; 13 | email: string; 14 | language: Language; 15 | locale: Locale; 16 | timezone: string; 17 | userGroups: Array; 18 | roles: Array; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/environment.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface definition for our application environment "variables", this is 3 | * used to "type cast" following files; 4 | * - src/environments/environment.prod.ts 5 | * - src/environments/environment.ts 6 | * 7 | * production 8 | * Is application in production more or not 9 | * 10 | * name 11 | * Environment name; dev, prod, etc. 12 | * 13 | * version 14 | * Application version, read from package.json on build time 15 | */ 16 | export interface EnvironmentInterface { 17 | production: boolean; 18 | name: string; 19 | version: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/components/oops/oops.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatAnchor } from '@angular/material/button'; 3 | import { TranslocoPipe } from '@jsverse/transloco'; 4 | import { FlexFillDirective, LayoutAlignDirective, LayoutDirective } from '@ngbracket/ngx-layout/flex'; 5 | 6 | @Component({ 7 | selector: 'app-oops', 8 | templateUrl: './oops.component.html', 9 | styleUrls: ['./oops.component.scss'], 10 | imports: [ 11 | FlexFillDirective, 12 | LayoutDirective, 13 | LayoutAlignDirective, 14 | MatAnchor, 15 | TranslocoPipe, 16 | ], 17 | }) 18 | 19 | export class OopsComponent { } 20 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/shared/factories/http-loader.factory.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 3 | 4 | /** 5 | * Factory for `TranslateHttpLoader` loader that we're using to load 6 | * translation JSON files. We need to use this custom factory for this 7 | * service to prevent HTTP cache within those translation JSON files. 8 | */ 9 | export const httpLoaderFactory = (httpClient: HttpClient): TranslateHttpLoader => { 10 | const ts = Math.round((new Date()).getTime() / 1000); 11 | 12 | return new TranslateHttpLoader(httpClient, './assets/i18n/', `.json?t=${ts}`); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/shared/components/version-change-dialog/version-change-dialog.component.html: -------------------------------------------------------------------------------- 1 |

4 | {{ 'component.version-change-dialog.title' | transloco }} 5 |

6 | 7 |
10 |

13 |
14 | 15 |
18 | 24 |
25 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/application-configuration-interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface definition for our application configuration which is initialized 3 | * before Angular itself is bootstrapped so that we can rely that we have all 4 | * configuration values initialized. 5 | * 6 | * See `/src/assets/config/config.*.json` files to see what those configuration 7 | * values actually are. 8 | * 9 | * apiUrl 10 | * Backend API url, eg. https://api.somedomain.tld 11 | * 12 | * tokenUrl 13 | * Backend API url where Json WebToken (JWT) can be obtained 14 | */ 15 | export interface ApplicationConfigurationInterface { 16 | apiUrl: string; 17 | tokenUrl: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/store/app.effects.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationEffects } from 'src/app/store/authentication/authentication.effects'; 2 | import { ErrorEffects } from 'src/app/store/error/error.effects'; 3 | import { LayoutEffects } from 'src/app/store/layout/layout.effects'; 4 | import { RouterEffects } from 'src/app/store/router/router.effects'; 5 | import { VersionEffects } from 'src/app/store/version/version.effects'; 6 | 7 | /** 8 | * Application NgRx effects that we using. These are used on main application 9 | * module definition. 10 | */ 11 | export const effects = [ 12 | AuthenticationEffects, 13 | ErrorEffects, 14 | LayoutEffects, 15 | RouterEffects, 16 | VersionEffects, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const appRoutes: Routes = [ 4 | { 5 | path: '', 6 | pathMatch: 'full', 7 | loadComponent: (): Promise => import('src/app/landing/landing.component').then(m => m.LandingComponent), 8 | }, 9 | { 10 | path: 'oops', 11 | pathMatch: 'full', 12 | loadComponent: (): Promise => import('src/app/shared/components/oops/oops.component').then(m => m.OopsComponent), 13 | }, 14 | { 15 | path: 'auth', 16 | loadChildren: (): Promise => 17 | import('src/app/auth/auth.routes').then(m => m.authRoutes), 18 | }, 19 | { 20 | path: '**', 21 | redirectTo: '/', 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/app/auth/login/login.routes.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { ActivatedRoute, Routes, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AnonymousGuard } from 'src/app/auth/guards'; 6 | import { LoginComponent } from 'src/app/auth/login/login.component'; 7 | 8 | export const loginRoutes: Routes = [ 9 | { 10 | path: 'login', 11 | canActivate: [ 12 | (): Observable => inject(AnonymousGuard).canActivate(inject(ActivatedRoute).snapshot), 13 | ], 14 | component: LoginComponent, 15 | children: [ 16 | { 17 | path: '**', 18 | redirectTo: 'login', 19 | }, 20 | ], 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/server-error-debug.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface definition for backend error debug trace. 3 | * 4 | * exception 5 | * Exception class name. 6 | * 7 | * file 8 | * Filename where this exception occurred. 9 | * 10 | * line 11 | * Line number where this exception occurred. 12 | * 13 | * message 14 | * Exception message. 15 | * 16 | * trace 17 | * Exception trace as an array. 18 | * 19 | * traceString 20 | * Exception trace as a string. 21 | */ 22 | export interface ServerErrorDebugInterface { 23 | exception: string; 24 | file: string; 25 | line: number; 26 | message: string; 27 | trace: Array; 28 | traceString: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shared/components/oops/oops.component.html: -------------------------------------------------------------------------------- 1 |
5 |
6 |

{{ 'component.oops.title' | transloco }}

7 | 8 |

¯\_(ツ)_/¯

9 | 10 |

{{ 'component.oops.subtitle' | transloco }}

11 | 12 | 17 | {{ 'component.oops.link' | transloco }} 18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/auth/interfaces/auth-guard-meta-data.interface'; 2 | export * from 'src/app/auth/interfaces/credentials-request.interface'; 3 | export * from 'src/app/auth/interfaces/credentials-response.interface'; 4 | export * from 'src/app/auth/interfaces/role.interface'; 5 | export * from 'src/app/auth/interfaces/role-guard-meta-data.interface'; 6 | export * from 'src/app/auth/interfaces/user-data.interface'; 7 | export * from 'src/app/auth/interfaces/user-data-value.interface'; 8 | export * from 'src/app/auth/interfaces/user-group.interface'; 9 | export * from 'src/app/auth/interfaces/user-profile.interface'; 10 | export * from 'src/app/auth/interfaces/user-profile-value.interface'; 11 | -------------------------------------------------------------------------------- /scripts/project-stats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | printf "%-15s %10s %14s\n" "Extension" "File count" "Line count" 4 | 5 | echo -e "-----------------------------------------" 6 | 7 | find ./src -type f -iname '*.*' | sed -n 's/.*\.//p' | tr '[:upper:]' '[:lower:]' | sort | uniq -c | while read count extension; do 8 | line_count=$(find ./src -name "*.$extension" -type f -print0 | xargs -0 cat | wc -l) 9 | printf "%-15s %10d %14d\n" ".$extension" "$count" "$line_count" 10 | done 11 | 12 | echo -e "-----------------------------------------" 13 | 14 | total_files=$(find ./src -type f | wc -l) 15 | total_lines=$(find ./src -type f -exec cat {} + | wc -l) 16 | 17 | printf "%-15s %10d %14d\n" "Total:" "$total_files" "$total_lines" 18 | -------------------------------------------------------------------------------- /src/app/store/version/version.state.ts: -------------------------------------------------------------------------------- 1 | import { ServerErrorAwareState } from 'src/app/store/aware-states'; 2 | 3 | /** 4 | * Interface definition for application version store contents. 5 | * 6 | * frontend 7 | * Frontend application version. 8 | * 9 | * backend 10 | * Backend application version. 11 | * 12 | * loadingFrontend 13 | * Is frontend application version loading or not. 14 | * 15 | * loadingBackend 16 | * Is backend application version loading or not. 17 | * 18 | * error 19 | * Latest error from loading frontend/backend version. 20 | */ 21 | export interface VersionState extends ServerErrorAwareState { 22 | frontend: string; 23 | backend: string; 24 | isLoadingFrontend: boolean; 25 | isLoadingBackend: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/error-message.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface definition for "generic" error message that is used on custom 3 | * `app-error-message` component. 4 | * 5 | * message 6 | * Backend error message OR translated error message from frontend 7 | * application - see that `app-error-message` component for details. 8 | * Also note that backend error message could be translated one. 9 | * 10 | * property 11 | * Name of the backend property which caused this error. 12 | * 13 | * debug 14 | * This is only present if using non-production version. 15 | */ 16 | export interface ErrorMessageInterface { 17 | message: string; 18 | property: string; 19 | debug?: string|null; // This is only present if using non-production version 20 | } 21 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | 3 | import { AppPage } from './app.po'; 4 | 5 | describe('workspace-project App', (): void => { 6 | let page: AppPage; 7 | 8 | beforeEach((): void => { 9 | page = new AppPage(); 10 | }); 11 | 12 | it('should display welcome message', (): void => { 13 | page.navigateTo(); 14 | expect(page.getTitleText()).toEqual('angular-frontend app is running!'); 15 | }); 16 | 17 | afterEach(async (): Promise => { 18 | // Assert that there are no errors emitted from the browser 19 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 20 | expect(logs).not.toContain(jasmine.objectContaining({ 21 | level: logging.Level.SEVERE, 22 | })); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This directory contains different documentation resources about this 4 | application. Each of those docs are covering different topic about this 5 | application. 6 | 7 | ## Table of Contents 8 | 9 | * [What is this](#what-is-this) 10 | * [Table of Contents](#table-of-contents) 11 | * [Resources](#resources-table-of-contents) 12 | 13 | ## Resources [ᐞ](#table-of-contents) 14 | 15 | * [Custom commands](COMMANDS.md) 16 | * [Concepts and features](CONCEPTS_AND_FEATURES.md) 17 | * [Dependency update](DEPENDENCY_UPDATE.md) 18 | * [Speed problems with application?](SPEED_UP_DOCKER_COMPOSE.md) 19 | * [Tools](TOOLS.md) 20 | * [Translations](TRANSLATIONS.md) 21 | * [Usage checklist](USAGE_CHECKLIST.md) 22 | 23 | --- 24 | 25 | [Back to main README.md](../README.md) 26 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/error-message-server.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface definition for client side error messages - server side error 3 | * messages are converted to this one on `app-error-message` component. 4 | * 5 | * code 6 | * UUID v4 error code from backend. 7 | * 8 | * message 9 | * Backend message for this error, note that this might be translated one. 10 | * 11 | * target 12 | * Target class / object where this error occurred. 13 | * 14 | * propertyPath 15 | * Property path of this error, usually just one level but may contain 16 | * multiple levels that are separated by dot (.) character. 17 | */ 18 | export interface ErrorMessageServerInterface { 19 | code: string; 20 | message: string; 21 | target: string; 22 | propertyPath: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/server-error.interface.ts: -------------------------------------------------------------------------------- 1 | import { ServerErrorDebugInterface } from 'src/app/shared/interfaces/server-error-debug.interface'; 2 | 3 | /** 4 | * Interface definition for backend errors. 5 | * 6 | * code 7 | * Error code, could be same as status or not - depending the error itself. 8 | * 9 | * message 10 | * Error message. 11 | * 12 | * status 13 | * HTTP status code. 14 | * 15 | * statusText 16 | * HTTP status code text. 17 | * 18 | * debug 19 | * Debug information, this is only present if backend is running on 20 | * development mode. 21 | */ 22 | export interface ServerErrorInterface { 23 | code: number; 24 | message: string; 25 | status: number; 26 | statusText: string; 27 | debug?: Array; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "strict": true, 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "declaration": false, 10 | "experimentalDecorators": true, 11 | "module": "es2020", 12 | "moduleResolution": "bundler", 13 | "importHelpers": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "target": "ES2022", 17 | "typeRoots": [ 18 | "node_modules/@types" 19 | ], 20 | "useDefineForClassFields": false 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true, 25 | "strictInputAccessModifiers": true, 26 | "strictTemplates": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/localization.interface.ts: -------------------------------------------------------------------------------- 1 | import { Language, Locale } from 'src/app/shared/enums'; 2 | 3 | /** 4 | * Interface definition for user localization settings. 5 | * 6 | * language 7 | * User language - see `src/app/shared/enums/language.enum.ts` for 8 | * possible values. This will affect to translations that we're using 9 | * in application. 10 | * 11 | * locale 12 | * User language - see `src/app/shared/enums/locale.enum.ts` for 13 | * possible values. This will affect number, date, etc. formatting 14 | * in application. 15 | * 16 | * timezone 17 | * User timezone. This will affect to date, datetime formatting in 18 | * application. 19 | */ 20 | export interface LocalizationInterface { 21 | language: Language; 22 | locale: Locale; 23 | timezone: string; 24 | } 25 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/auth/factories/jwt-options.factory.ts: -------------------------------------------------------------------------------- 1 | import { JwtModuleOptions } from '@auth0/angular-jwt'; 2 | import { LocalStorageService } from 'ngx-webstorage'; 3 | 4 | import { ConfigurationService } from 'src/app/shared/services'; 5 | 6 | /** 7 | * Function to determine Json Web Token module options according to our current 8 | * application configuration. 9 | */ 10 | export const jwtOptionsFactory = (localStorage: LocalStorageService): JwtModuleOptions['config'] => ({ 11 | tokenGetter: (): string => localStorage.retrieve('token') ?? '', // Get token from local storage 12 | allowedDomains: [ // Allowed domains with Json Web Token 13 | new URL(ConfigurationService.configuration.apiUrl).host, 14 | ], 15 | disallowedRoutes: [ // Disallowed routes for Json Web Token 16 | ConfigurationService.configuration.tokenUrl, 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/shared/directives/auto-focus.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input, OnChanges, SimpleChanges, inject } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appAutoFocus]', 5 | }) 6 | 7 | export class AutoFocusDirective implements OnChanges { 8 | private readonly hostElement: ElementRef = inject(ElementRef); 9 | 10 | @Input() public appAutoFocus: boolean = false; 11 | 12 | /** 13 | * A callback method that is invoked immediately after the default change 14 | * detector has checked data-bound properties if at least one has changed, 15 | * and before the view and content children are checked. 16 | */ 17 | public ngOnChanges(changes: SimpleChanges): void { 18 | if (changes.appAutoFocus.currentValue === true) { 19 | setTimeout((): void => this.hostElement.nativeElement.focus(), 0); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | 3 | import { AcceptLanguageInterceptor } from 'src/app/shared/interceptors/accept-language.interceptor'; 4 | import { BackendVersionInterceptor } from 'src/app/shared/interceptors/backend-version.interceptor'; 5 | import { ErrorInterceptor } from 'src/app/shared/interceptors/error.interceptor'; 6 | import { HttpCacheInterceptor } from 'src/app/shared/interceptors/http-cache.interceptor'; 7 | 8 | export const httpInterceptors = [ 9 | { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }, 10 | { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, 11 | { provide: HTTP_INTERCEPTORS, useClass: AcceptLanguageInterceptor, multi: true }, 12 | { provide: HTTP_INTERCEPTORS, useClass: BackendVersionInterceptor, multi: true }, 13 | ]; 14 | -------------------------------------------------------------------------------- /src/app/store/layout/layout.type.ts: -------------------------------------------------------------------------------- 1 | // Layout store action definitions. 2 | export enum LayoutType { 3 | CHANGE_LANGUAGE = '[Layout] Change language', 4 | CHANGE_LOCALE = '[Layout] Change locale', 5 | CHANGE_TIMEZONE = '[Layout] Change timezone', 6 | CHANGE_THEME = '[Layout] Change theme', 7 | UPDATE_LOCALIZATION = '[Layout] Update localization', 8 | SET_LANGUAGE = '[Layout] Set language', 9 | CHANGE_VIEWPORT = '[Layout] Change viewport', 10 | SCROLL_TO = '[Layout] Scroll to anchor', 11 | SCROLL_TO_TOP = '[Layout] Scroll to top of the page', 12 | CLEAR_SCROLL_TO = '[Layout] Clear scroll to', 13 | SNACKBAR_MESSAGE = '[Layout] Show snackbar message', 14 | } 15 | 16 | export type LocalizationTypes = LayoutType.CHANGE_LANGUAGE 17 | | LayoutType.CHANGE_LOCALE 18 | | LayoutType.CHANGE_TIMEZONE; 19 | -------------------------------------------------------------------------------- /src/app/store/meta-reducers/local-storage-sync.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer } from '@ngrx/store'; 2 | import { LocalStorageConfig, localStorageSync } from 'ngrx-store-localstorage'; 3 | 4 | /** 5 | * Meta reducer to keep store state in session storage. This will ensure that 6 | * specified parts of our store are stored to `sessionStorage` and if/when user 7 | * reloads (hits eg. F5) store state will be initialized with previous data. 8 | */ 9 | export const localStorageSyncReducer = (reducer: ActionReducer): ActionReducer => { 10 | const config: LocalStorageConfig = { 11 | keys: [ 12 | 'layout', 13 | ], 14 | rehydrate: true, 15 | storage: sessionStorage, 16 | restoreDates: false, 17 | storageKeySerializer: (key: string): string => `store_${key}`, 18 | }; 19 | 20 | return localStorageSync(config)(reducer); 21 | }; 22 | -------------------------------------------------------------------------------- /.idea/copilot.data.migration.agent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | groups: 8 | action-dependencies: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: npm 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | groups: 16 | angular-dependencies: 17 | patterns: 18 | - "@angular*" 19 | ngrx-dependencies: 20 | patterns: 21 | - "@ngrx*" 22 | type-dependencies: 23 | patterns: 24 | - "@types*" 25 | npm-dependencies: 26 | patterns: 27 | - "*" 28 | exclude-patterns: 29 | - "@angular*" 30 | - "@ngrx*" 31 | - "@types*" 32 | - package-ecosystem: docker 33 | directory: "/" 34 | schedule: 35 | interval: daily 36 | groups: 37 | docker-dependencies: 38 | patterns: 39 | - "*" 40 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/error-message-client.interface.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessageServerInterface } from 'src/app/shared/interfaces/error-message-server.interface'; 2 | 3 | /** 4 | * Interface definition for client side error messages - server side error 5 | * messages are converted to this one on `app-error-message` component. 6 | * 7 | * messageText 8 | * Error message from backend. 9 | * 10 | * messageProperty 11 | * Property message from backend. 12 | * 13 | * messageTextClient 14 | * If not null, frontend application has translation for this 15 | * 16 | * messagePropertyClient 17 | * If not null, frontend application has translation for this 18 | */ 19 | export interface ErrorMessageClientInterface extends ErrorMessageServerInterface { 20 | messageText: string; 21 | messageProperty: string; 22 | messageTextClient?: string|null; 23 | messagePropertyClient?: string|null; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/store/app.reducers.ts: -------------------------------------------------------------------------------- 1 | import { routerReducer } from '@ngrx/router-store'; 2 | import { ActionReducerMap, MetaReducer } from '@ngrx/store'; 3 | 4 | import { AppState } from 'src/app/store/app.state'; 5 | import { 6 | authenticationReducer, 7 | errorReducer, 8 | layoutReducer, 9 | localStorageSyncReducer, 10 | versionReducer, 11 | } from 'src/app/store/store.reducers'; 12 | import { environment } from 'src/environments/environment'; 13 | 14 | /** 15 | * Application NgRx reducers that we are using within this application. 16 | */ 17 | export const reducers: ActionReducerMap = { 18 | router: routerReducer, 19 | authentication: authenticationReducer, 20 | error: errorReducer, 21 | layout: layoutReducer, 22 | version: versionReducer, 23 | }; 24 | 25 | export const metaReducers: Array> = environment.production ? [localStorageSyncReducer] : [localStorageSyncReducer]; 26 | -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file purpose is just to write `src/assets/version.json` file that 3 | * contains current version information about this application. This file 4 | * is executed on `postinstall` process. 5 | * 6 | * This static file will be used later on to check if frontend application 7 | * has been updated during user has been used it without reloading page 8 | * itself. 9 | */ 10 | 'use strict'; 11 | 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | const dir = './src/assets'; 15 | const filePath = path.join(dir, 'version.json'); 16 | 17 | // Create directory if it doesn't exist 18 | if (!fs.existsSync(dir)) { 19 | fs.mkdirSync(dir, { recursive: true }); 20 | } 21 | 22 | const data = { 23 | version: require('./package.json').version, 24 | }; 25 | 26 | fs.writeFile(filePath, JSON.stringify(data, null, ' ') + '\n', (error) => { 27 | if (error) { 28 | throw error; 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /.github/workflows/vulnerability-scan.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions docs 2 | # https://help.github.com/en/articles/about-github-actions 3 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 4 | name: Vulnerability Scan 5 | on: 6 | schedule: 7 | - cron: '0 12 * * *' # Run every day at 12:00 UTC 8 | workflow_dispatch: 9 | 10 | jobs: 11 | scan: 12 | name: Scan docker image with trivy 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v6.0.0 17 | 18 | - name: Build the Docker image 19 | run: docker build . --file Dockerfile --tag angular-ngrx-frontend:master 20 | 21 | - name: Scan image with trivy 22 | uses: aquasecurity/trivy-action@0.33.1 23 | with: 24 | image-ref: angular-ngrx-frontend:master 25 | ignore-unfixed: 'true' 26 | exit-code: '1' 27 | vuln-type: 'os,library' 28 | severity: 'CRITICAL,HIGH' 29 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | /* 6 | * For easier debugging in development mode, you can import the following file 7 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 8 | * 9 | * This import should be commented out in production mode because it will have a negative impact 10 | * on performance if an error is thrown. 11 | */ 12 | // import 'zone.js/plugins/zone-error'; 13 | 14 | import packageInfo from 'package.json'; 15 | import { EnvironmentInterface } from 'src/app/shared/interfaces'; // Included with Angular CLI. 16 | 17 | export const environment: EnvironmentInterface = { 18 | production: false, 19 | name: 'dev', 20 | version: packageInfo.version, 21 | }; 22 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /src/environments/environment.local-prod.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | /* 6 | * For easier debugging in development mode, you can import the following file 7 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 8 | * 9 | * This import should be commented out in production mode because it will have a negative impact 10 | * on performance if an error is thrown. 11 | */ 12 | // import 'zone.js/plugins/zone-error'; 13 | 14 | import packageInfo from 'package.json'; 15 | import { EnvironmentInterface } from 'src/app/shared/interfaces'; // Included with Angular CLI. 16 | 17 | export const environment: EnvironmentInterface = { 18 | production: true, 19 | name: 'dev', 20 | version: packageInfo.version, 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/role-guard-meta-data.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for role guard meta data, which is used on following guards within 3 | * this application; 4 | * - RoleAdminGuard 5 | * - RoleALoggedGuard 6 | * - RoleRootGuard 7 | * - RoleUserGuard 8 | * 9 | * With this you can change the guard behaviour by specified route `data` 10 | * property. For detailed information and actual implementation you could 11 | * look `src/app/auth/guards/base-role.ts` file. 12 | * 13 | * redirect 14 | * Should guard redirect user or not, defaults to true 15 | * 16 | * routeNotLoggedIn 17 | * Route where to redirect if user is not logged in, defaults to `/auth/login` 18 | * 19 | * routeNoRole 20 | * Route where to redirect user if he/she doesn't have specified role, 21 | * defaults to `/` 22 | */ 23 | export interface RoleGuardMetaDataInterface { 24 | redirect: boolean; 25 | routeNotLoggedIn: string; 26 | routeNoRole: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/store/authentication/authentication.state.ts: -------------------------------------------------------------------------------- 1 | import { UserDataInterface, UserProfileInterface } from 'src/app/auth/interfaces'; 2 | import { IsLoadingAwareState, ServerErrorAwareState } from 'src/app/store/aware-states'; 3 | 4 | /** 5 | * Interface definition for our authentication store contents. 6 | * 7 | * isLoading 8 | * Is store in "loading" state or not, this will be `true` when we're 9 | * making login or fetching users profile. 10 | * 11 | * isLoggedIn 12 | * Is current user logged in to application or not. 13 | * 14 | * userData 15 | * User data from Json Web Token (JWT), can be null. 16 | * 17 | * profile 18 | * User profile data from backend, can be null. 19 | * 20 | * error 21 | * Possible error from backend, can be null. 22 | */ 23 | export interface AuthenticationState extends IsLoadingAwareState, ServerErrorAwareState { 24 | isLoggedIn: boolean; 25 | userData: UserDataInterface|null; 26 | profile: UserProfileInterface|null; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/store/router/router.selectors.ts: -------------------------------------------------------------------------------- 1 | import { RouterReducerState, getRouterSelectors } from '@ngrx/router-store'; 2 | import { createFeatureSelector } from '@ngrx/store'; 3 | 4 | /** 5 | * Selectors for `RouterState` store. 6 | * 7 | * Simple usage example; 8 | * 9 | * public constructor(private store: Store) { 10 | * this.subscription = new Subscription(); 11 | * } 12 | * 13 | * public ngOnInit(): void { 14 | * this.subscription 15 | * .add(this.store 16 | * .select(routerSelectors.selectCurrentRoute) 17 | * .subscribe((currentRoute: ActivatedRoute): void => { 18 | * ... 19 | * }), 20 | * ); 21 | * } 22 | * 23 | * public ngOnDestroy(): void { 24 | * this.subscription.unsubscribe(); 25 | * } 26 | */ 27 | 28 | // Feature selector for `layout` store 29 | const selectFeature = createFeatureSelector('router'); 30 | 31 | export const routerSelectors = { 32 | ...getRouterSelectors(selectFeature), 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/auth/guards/index.ts: -------------------------------------------------------------------------------- 1 | import { AnonymousGuard } from 'src/app/auth/guards/anonymous.guard'; 2 | import { AuthenticationGuard } from 'src/app/auth/guards/authentication.guard'; 3 | import { RoleAdminGuard } from 'src/app/auth/guards/role-admin.guard'; 4 | import { RoleALoggedGuard } from 'src/app/auth/guards/role-logged.guard'; 5 | import { RoleRootGuard } from 'src/app/auth/guards/role-root.guard'; 6 | import { RoleUserGuard } from 'src/app/auth/guards/role-user.guard'; 7 | 8 | export * from 'src/app/auth/guards/anonymous.guard'; 9 | export * from 'src/app/auth/guards/authentication.guard'; 10 | export * from 'src/app/auth/guards/role-admin.guard'; 11 | export * from 'src/app/auth/guards/role-logged.guard'; 12 | export * from 'src/app/auth/guards/role-root.guard'; 13 | export * from 'src/app/auth/guards/role-user.guard'; 14 | 15 | export const authenticationGuards = [ 16 | AnonymousGuard, 17 | AuthenticationGuard, 18 | RoleAdminGuard, 19 | RoleALoggedGuard, 20 | RoleRootGuard, 21 | RoleUserGuard, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/shared/services/http-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { HttpCacheType } from 'src/app/shared/types'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class HttpCacheService { 10 | private requests: HttpCacheType; 11 | 12 | public constructor() { 13 | this.requests = {}; 14 | } 15 | 16 | public get(url: string): HttpResponse|undefined { 17 | if (this.requests[url] && this.requests[url].timestamp + 600 * 1000 < Date.now()) { 18 | this.invalidateUrl(url); 19 | } 20 | 21 | return this.requests[url]?.response; 22 | } 23 | 24 | public store(url: string, response: HttpResponse): void { 25 | this.requests[url] = { 26 | response, 27 | timestamp: Date.now(), 28 | }; 29 | } 30 | 31 | public invalidate(): void { 32 | this.requests = {}; 33 | } 34 | 35 | public invalidateUrl(url: string): void { 36 | delete this.requests[url]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | compose.*.yaml 4 | 5 | # compiled output 6 | /dist 7 | /tmp 8 | /out-tsc 9 | # Only exists if Bazel was run 10 | /bazel-out 11 | 12 | # dependencies 13 | /node_modules 14 | 15 | # profiling files 16 | chrome-profiler-events*.json 17 | speed-measure-plugin*.json 18 | 19 | # User-specific stuff: 20 | .idea/**/workspace.xml 21 | .idea/**/tasks.xml 22 | .idea/copilot 23 | .idea/dictionaries 24 | .idea/shelf 25 | .idea/php.xml 26 | 27 | # IDEs and editors 28 | .project 29 | .classpath 30 | .c9/ 31 | *.launch 32 | .settings/ 33 | *.sublime-workspace 34 | 35 | # IDE - VSCode 36 | .vscode/* 37 | !.vscode/settings.json 38 | !.vscode/tasks.json 39 | !.vscode/launch.json 40 | !.vscode/extensions.json 41 | .history/* 42 | 43 | # misc 44 | /.angular/cache 45 | /.sass-cache 46 | /connect.lock 47 | /coverage 48 | /libpeerconnection.log 49 | npm-debug.log 50 | yarn-error.log 51 | testem.log 52 | /typings 53 | 54 | # System Files 55 | .DS_Store 56 | Thumbs.db 57 | -------------------------------------------------------------------------------- /src/app/store/authentication/authentication.type.ts: -------------------------------------------------------------------------------- 1 | import { LayoutType } from 'src/app/store'; 2 | 3 | // Authentication store action type definitions. 4 | export enum AuthenticationType { 5 | LOGIN = '[Authentication] Login', 6 | LOGIN_SUCCESS = '[Authentication] Login success', 7 | LOGIN_FAILURE = '[Authentication] Login failure', 8 | PROFILE = '[Authentication] Profile', 9 | PROFILE_SUCCESS = '[Authentication] Profile success', 10 | PROFILE_FAILURE = '[Authentication] Profile failure', 11 | LOGOUT = '[Authentication] Logout', 12 | RESET_ERROR = '[Authentication] Reset error', 13 | } 14 | 15 | // Authentication login types 16 | export type AuthenticationLoginTypes = AuthenticationType.LOGIN_SUCCESS 17 | | AuthenticationType.LOGIN_FAILURE; 18 | 19 | // Authentication profile types 20 | export type AuthenticationProfileTypes = AuthenticationType.PROFILE_SUCCESS 21 | | AuthenticationType.PROFILE_FAILURE; 22 | 23 | export type AuthenticationLoginSuccessTypes = AuthenticationType.PROFILE 24 | | LayoutType.UPDATE_LOCALIZATION; 25 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/app/shared/interfaces/application-configuration-interface'; 2 | export * from 'src/app/shared/interfaces/dictionary.interface'; 3 | export * from 'src/app/shared/interfaces/environment.interface'; 4 | export * from 'src/app/shared/interfaces/error-message.interface'; 5 | export * from 'src/app/shared/interfaces/error-message-client.interface'; 6 | export * from 'src/app/shared/interfaces/error-message-server.interface'; 7 | export * from 'src/app/shared/interfaces/language-value.interface'; 8 | export * from 'src/app/shared/interfaces/locale-value.interface'; 9 | export * from 'src/app/shared/interfaces/localization.interface'; 10 | export * from 'src/app/shared/interfaces/server-error.interface'; 11 | export * from 'src/app/shared/interfaces/server-error-debug.interface'; 12 | export * from 'src/app/shared/interfaces/server-error-value.interface'; 13 | export * from 'src/app/shared/interfaces/theme-value.interface'; 14 | export * from 'src/app/shared/interfaces/version.interface'; 15 | export * from 'src/app/shared/interfaces/viewport-value.interface'; 16 | -------------------------------------------------------------------------------- /src/app/shared/components/version-change-dialog/version-change-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { MatButton } from '@angular/material/button'; 3 | import { 4 | MAT_DIALOG_DATA, 5 | MatDialogTitle, 6 | MatDialogContent, 7 | MatDialogActions, 8 | MatDialogClose, 9 | } from '@angular/material/dialog'; 10 | import { TranslocoDirective, TranslocoPipe } from '@jsverse/transloco'; 11 | 12 | import { VersionContentInterface } from 'src/app/shared/components/version-change-dialog/version-content.interface'; 13 | 14 | @Component({ 15 | selector: 'app-version-change-dialog', 16 | templateUrl: './version-change-dialog.component.html', 17 | styleUrls: ['./version-change-dialog.component.scss'], 18 | imports: [ 19 | MatDialogTitle, 20 | MatDialogContent, 21 | TranslocoDirective, 22 | MatDialogActions, 23 | MatButton, 24 | MatDialogClose, 25 | TranslocoPipe, 26 | ], 27 | }) 28 | export class VersionChangeDialogComponent { 29 | public readonly data: VersionContentInterface = inject(MAT_DIALOG_DATA); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/store/router/router.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, inject } from '@angular/core'; 2 | import { Actions, createEffect, ofType } from '@ngrx/effects'; 3 | import { routerNavigatedAction } from '@ngrx/router-store'; 4 | import { Action } from '@ngrx/store'; 5 | import { Observable } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | import { LayoutType, layoutActions } from 'src/app/store'; 9 | 10 | @Injectable() 11 | export class RouterEffects { 12 | private readonly actions$: Actions = inject(Actions); 13 | 14 | // noinspection JSUnusedLocalSymbols 15 | /** 16 | * Each time `NavigationEnd` event is dispatched from router, we want to 17 | * scroll browser to top of the page. This basically happens each time user 18 | * navigates to another route within this application. 19 | */ 20 | private routerNavigatedActionEffect$: Observable> = createEffect( 21 | (): Observable> => this.actions$.pipe( 22 | ofType(routerNavigatedAction), 23 | map((): Action => layoutActions.scrollToTop()), 24 | ), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/auth/guards/role-root.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { BaseRole } from 'src/app/auth/guards/base-role'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class RoleRootGuard extends BaseRole { 12 | /** 13 | * Purpose of this guard is to check that user has `Role.ROLE_ROOT` or not. 14 | * This method is used within route definition `canActivate` definition. 15 | */ 16 | public canActivate(route: ActivatedRouteSnapshot): Observable { 17 | return this.checkRole(Role.ROLE_ROOT, route.data?.roleGuardMeta ?? null); 18 | } 19 | 20 | /** 21 | * Purpose of this guard is to check that user has `Role.ROLE_ROOT` or not. 22 | * This method is used within route definition `canActivateChild` definition. 23 | */ 24 | public canActivateChild(childRoute: ActivatedRouteSnapshot): Observable { 25 | return this.checkRole(Role.ROLE_ROOT, childRoute.data?.roleGuardMeta ?? null); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/auth/guards/role-user.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { BaseRole } from 'src/app/auth/guards/base-role'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class RoleUserGuard extends BaseRole { 12 | /** 13 | * Purpose of this guard is to check that user has `Role.ROLE_USER` or not. 14 | * This method is used within route definition `canActivate` definition. 15 | */ 16 | public canActivate(route: ActivatedRouteSnapshot): Observable { 17 | return this.checkRole(Role.ROLE_USER, route.data?.roleGuardMeta ?? null); 18 | } 19 | 20 | /** 21 | * Purpose of this guard is to check that user has `Role.ROLE_USER` or not. 22 | * This method is used within route definition `canActivateChild` definition. 23 | */ 24 | public canActivateChild(childRoute: ActivatedRouteSnapshot): Observable { 25 | return this.checkRole(Role.ROLE_USER, childRoute.data?.roleGuardMeta ?? null); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Tarmo Leppänen 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 | -------------------------------------------------------------------------------- /src/app/auth/guards/role-admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { BaseRole } from 'src/app/auth/guards/base-role'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class RoleAdminGuard extends BaseRole { 12 | /** 13 | * Purpose of this guard is to check that user has `Role.ROLE_ADMIN` or not. 14 | * This method is used within route definition `canActivate` definition. 15 | */ 16 | public canActivate(route: ActivatedRouteSnapshot): Observable { 17 | return this.checkRole(Role.ROLE_ADMIN, route.data?.roleGuardMeta ?? null); 18 | } 19 | 20 | /** 21 | * Purpose of this guard is to check that user has `Role.ROLE_ADMIN` or not. 22 | * This method is used within route definition `canActivateChild` definition. 23 | */ 24 | public canActivateChild(childRoute: ActivatedRouteSnapshot): Observable { 25 | return this.checkRole(Role.ROLE_ADMIN, childRoute.data?.roleGuardMeta ?? null); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/store/error/error.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, createReducer, on } from '@ngrx/store'; 2 | 3 | import { ServerErrorValueInterface } from 'src/app/shared/interfaces'; 4 | import { ErrorState, errorActions } from 'src/app/store'; 5 | 6 | // Initial state of `Error` store. 7 | const initialState: ErrorState = { 8 | errorSnackbar: null, 9 | }; 10 | 11 | const reducer = createReducer( 12 | initialState, 13 | // Action to store specified error to store. 14 | on( 15 | errorActions.showSnackbar, 16 | errorActions.showSnackbar, 17 | errorActions.showSnackbar, 18 | errorActions.showSnackbar, 19 | (state: ErrorState, { error }: ServerErrorValueInterface): ErrorState => ({ 20 | ...state, 21 | errorSnackbar: error, 22 | }), 23 | ), 24 | // Action to clear current snackbar error in store. 25 | on( 26 | errorActions.clearSnackbar, 27 | (state: ErrorState): ErrorState => ({ 28 | ...state, 29 | errorSnackbar: null, 30 | }), 31 | ), 32 | ); 33 | 34 | // Export error store reducer. 35 | export const errorReducer = (state: ErrorState|undefined, action: Action): ErrorState => reducer(state, action); 36 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/angular-frontend'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /docker/ssl/tls.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC8jCCAdoCAQAwgawxCzAJBgNVBAYTAkZJMRMwEQYDVQQIDApTb21lLVN0YXRl 3 | MTkwNwYDVQQKDDBodHRwczovL2dpdGh1Yi5jb20vdGFybGVwcC9hbmd1bGFyLW5n 4 | cngtZnJvbnRlbmQxOTA3BgNVBAsMMGh0dHBzOi8vZ2l0aHViLmNvbS90YXJsZXBw 5 | L2FuZ3VsYXItbmdyeC1mcm9udGVuZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjAN 6 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1Ijl983UOUIFVTlWLNMxVwi3jp7 7 | 2xAR93Kd4cIZA2mhkMmCekq0wQPOnjSvJD/n9g6HfRyt9N1ACXWIPWoaABSbOyw3 8 | BmPR1x3QsQOvmwq9OTFRSYS2SwOlyz1972xGMdwCLu2h5L6wLZbq7rYPv7Zj3BgL 9 | gWV0171qFR1OrsldC5sShv4C6hnTX2K0N7C86k0za3lldB7I7eGkxTntsNRFVO2L 10 | Uok+jfpmk4UC8dISmGWqpM3CahA/1Wol4nn4ohwZX5ZRrloGGIa8k/FkiFOfCPY7 11 | /SiiqSx8HKu4N/D6HDu7RoqeUDJWg5BScec9W5Rabp7Vt+eKxZwHLqeb6QIDAQAB 12 | oAAwDQYJKoZIhvcNAQELBQADggEBAFcyUwgR+vpC0QT/ya3EnH7mOG9aUR4zGok0 13 | DuBGdRmwFV16kYB8UcZVBDbUm+0RsE/+tTFol54ZWn2WNqLQqWLz/uasXIHc53nY 14 | nXegwG/4kbsGsBa5mv/kiVWmpMhuMhPk0iFX7ua0Rp/ecA/2BcH+jH9bkXmLGKUu 15 | XVb4q0hiTA9qv7BXmN9sDysxBEGva7I00g5ruVz6WjQzPtaNct4LkQonquFLVpxO 16 | yEKWd5Gax/iRBGoqZgoM5YGyT53MdDqZVf9JV21ClkgEttTyzYE5K5nWjNxJpm/W 17 | vu3XPOUROlppqnme9AYlmaJpFIlQ1txL6vNO+yq2ukoPWPeBPgE= 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /src/app/auth/guards/role-logged.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { BaseRole } from 'src/app/auth/guards/base-role'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class RoleALoggedGuard extends BaseRole { 12 | /** 13 | * Purpose of this guard is to check that user has `Role.ROLE_LOGGED` or not. 14 | * This method is used within route definition `canActivate` definition. 15 | */ 16 | public canActivate(route: ActivatedRouteSnapshot): Observable { 17 | return this.checkRole(Role.ROLE_LOGGED, route.data?.roleGuardMeta ?? null); 18 | } 19 | 20 | /** 21 | * Purpose of this guard is to check that user has `Role.ROLE_LOGGED` or not. 22 | * This method is used within route definition `canActivateChild` definition. 23 | */ 24 | public canActivateChild(childRoute: ActivatedRouteSnapshot): Observable { 25 | return this.checkRole(Role.ROLE_LOGGED, childRoute.data?.roleGuardMeta ?? null); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/shared/components/error-message/error-message.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | @for (error of errors; track error) { 5 | 6 |
9 | {{ error.property | transloco }} 10 | 11 |  -  12 | 13 | {{ error.message | transloco }} 14 | 15 | @if (!production) { 16 | 17 |  ({{ error.debug }}) 18 | 19 | } 20 |
21 |
22 | } 23 |
24 |
25 | 26 |
29 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /src/styles/animations.scss: -------------------------------------------------------------------------------- 1 | .ghost { 2 | animation: ghost-pulse 2s ease infinite; 3 | 4 | &--text { 5 | color: transparent !important; 6 | text-shadow: 0 0 8px var(--theme-text-color); 7 | } 8 | 9 | &--text-primary { 10 | color: transparent !important; 11 | text-shadow: 0 0 8px var(--theme-primary-color); 12 | } 13 | 14 | &--text-primary-contrast { 15 | color: transparent !important; 16 | text-shadow: 0 0 8px var(--theme-primary-color-contrast); 17 | } 18 | 19 | &--text-accent { 20 | color: transparent !important; 21 | text-shadow: 0 0 8px var(--theme-accent-color); 22 | } 23 | 24 | &--text-accent-contrast { 25 | color: transparent !important; 26 | text-shadow: 0 0 8px var(--theme-accent-color-contrast); 27 | } 28 | 29 | &--text-warn { 30 | color: transparent !important; 31 | text-shadow: 0 0 8px var(--theme-warn-color); 32 | } 33 | 34 | &--text-warn-contrast { 35 | color: transparent !important; 36 | text-shadow: 0 0 8px var(--theme-warn-color-contrast); 37 | } 38 | } 39 | 40 | @keyframes ghost-pulse { 41 | 0%, 42 | 100% { 43 | opacity: .67; 44 | } 45 | 46 | 50% { 47 | opacity: .17; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/landing/landing.component.ts: -------------------------------------------------------------------------------- 1 | import { AsyncPipe } from '@angular/common'; 2 | import { Component, inject } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { Device, Viewport } from 'src/app/shared/enums'; 7 | import { layoutSelectors } from 'src/app/store'; 8 | 9 | @Component({ 10 | selector: 'app-landing', 11 | templateUrl: './landing.component.html', 12 | styleUrls: [ 13 | './landing.component.scss', 14 | ], 15 | imports: [ 16 | AsyncPipe, 17 | ], 18 | }) 19 | 20 | export class LandingComponent { 21 | public readonly viewport$: Observable; 22 | public readonly device$: Observable; 23 | 24 | private readonly store: Store = inject(Store); 25 | 26 | /** 27 | * Constructor of the class, where we DI all services that we need to use 28 | * within this component and initialize needed properties. 29 | */ 30 | public constructor() { 31 | // Initialize `viewport$` and `device$` observables - remove these if you don't need these 32 | this.viewport$ = this.store.select(layoutSelectors.selectViewport); 33 | this.device$ = this.store.select(layoutSelectors.selectDevice); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/auth/enums/role.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * These role enums presents the same roles that you have in your backend. 3 | * 4 | * Logged in user Json Web Token (JWT) contains currently logged in user roles 5 | * that you can use to hide/show some elements etc. within your frontend 6 | * application. 7 | * 8 | * You can fetch currently logged in user roles by following code examples. 9 | * 10 | * Authentication store; 11 | * constructor(private store: Store) { } 12 | * 13 | * public ngOnInit(): void { 14 | * this.subscription 15 | * .add(this.store 16 | * .select(authenticationSelectors.roles) 17 | * .subscribe((roles: Array): Array => this.roles = roles), 18 | * ); 19 | * } 20 | * 21 | * Authentication service; 22 | * constructor(private authenticationService: AuthenticationService) { } 23 | * 24 | * public ngOnInit(): void { 25 | * this.subscription 26 | * .add(this.authenticationService.getLoggedInRoles() 27 | * .subscribe((roles: Array): Array|null => this.rolesB = roles), 28 | * ); 29 | * } 30 | */ 31 | export enum Role { 32 | ROLE_LOGGED = 'ROLE_LOGGED', 33 | ROLE_USER = 'ROLE_USER', 34 | ROLE_ADMIN = 'ROLE_ADMIN', 35 | ROLE_ROOT = 'ROLE_ROOT', 36 | ROLE_API = 'ROLE_API', 37 | } 38 | -------------------------------------------------------------------------------- /src/app/store/version/version.type.ts: -------------------------------------------------------------------------------- 1 | // Version store action definitions. 2 | export enum VersionType { 3 | FETCH_VERSIONS = '[Version] Fetch versions', 4 | FETCH_FRONTEND_VERSION = '[Version] Fetch frontend version', 5 | FETCH_FRONTEND_VERSION_SUCCESS = '[Version] Fetch frontend version success', 6 | FETCH_FRONTEND_VERSION_FAILURE = '[Version] Fetch frontend version failure', 7 | FETCH_BACKEND_VERSION = '[Version] Fetch backend version', 8 | FETCH_BACKEND_VERSION_SUCCESS = '[Version] Fetch backend version success', 9 | FETCH_BACKEND_VERSION_FAILURE = '[Version] Fetch backend version failure', 10 | NEW_BACKEND_VERSION = '[Version] New backend version - check if frontend version is also changed', 11 | } 12 | 13 | export type FetchVersionsTypes = VersionType.FETCH_BACKEND_VERSION 14 | | VersionType.FETCH_FRONTEND_VERSION; 15 | 16 | // Frontend version types 17 | export type FrontendVersionTypes = VersionType.FETCH_FRONTEND_VERSION_SUCCESS 18 | | VersionType.FETCH_FRONTEND_VERSION_FAILURE; 19 | 20 | // Backend version types 21 | export type BackendVersionTypes = VersionType.FETCH_BACKEND_VERSION_SUCCESS 22 | | VersionType.FETCH_BACKEND_VERSION_FAILURE; 23 | 24 | export type NewBackendVersionTypes = VersionType.FETCH_FRONTEND_VERSION 25 | | VersionType.FETCH_BACKEND_VERSION_SUCCESS; 26 | -------------------------------------------------------------------------------- /src/app/auth/guards/anonymous.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { BaseAuth } from 'src/app/auth/guards/base-auth'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AnonymousGuard extends BaseAuth { 11 | /** 12 | * Purpose of this guard is check that current user has not been authenticated 13 | * to application. If user is authenticated he/she is redirected to application 14 | * root. 15 | * 16 | * This method is used within route definition `canActivate` definition. 17 | */ 18 | public canActivate(route: ActivatedRouteSnapshot): Observable { 19 | return this.makeCheck(false, route.data?.authGuardMeta ?? null); 20 | } 21 | 22 | /** 23 | * Purpose of this guard is check that current user has not been authenticated 24 | * to application. If user is authenticated he/she is redirected to application 25 | * root. 26 | * 27 | * This method is used within route definition `canActivateChild` definition. 28 | */ 29 | public canActivateChild(childRoute: ActivatedRouteSnapshot): Observable { 30 | return this.makeCheck(false, childRoute.data?.authGuardMeta ?? null); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/auth/guards/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { BaseAuth } from 'src/app/auth/guards/base-auth'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AuthenticationGuard extends BaseAuth { 11 | /** 12 | * Purpose of this guard is check that current user has been authenticated to 13 | * application. If user is not authenticated he/she is redirected to application 14 | * login page. 15 | * 16 | * This method is used within route definition `canActivate` definition. 17 | */ 18 | public canActivate(route: ActivatedRouteSnapshot): Observable { 19 | return this.makeCheck(true, route.data?.authGuardMeta ?? null); 20 | } 21 | 22 | /** 23 | * Purpose of this guard is check that current user has been authenticated to 24 | * application. If user is not authenticated he/she is redirected to application 25 | * login page. 26 | * 27 | * This method is used within route definition `canActivateChild` definition. 28 | */ 29 | public canActivateChild(childRoute: ActivatedRouteSnapshot): Observable { 30 | return this.makeCheck(true, childRoute.data?.authGuardMeta ?? null); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/store/error/error.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, inject } from '@angular/core'; 2 | import { Actions, createEffect, ofType } from '@ngrx/effects'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { ServerErrorInterface } from 'src/app/shared/interfaces'; 7 | import { SnackbarService } from 'src/app/shared/services'; 8 | import { errorActions } from 'src/app/store'; 9 | 10 | @Injectable() 11 | export class ErrorEffects { 12 | private readonly actions$: Actions = inject(Actions); 13 | private readonly snackbarService: SnackbarService = inject(SnackbarService); 14 | 15 | // noinspection JSUnusedLocalSymbols 16 | /** 17 | * NgRx effect that is triggered within `ErrorType.SHOW_SNACKBAR` action. 18 | * This effect will call `SnackbarService` to create snackbar message 19 | * about specified error. 20 | * 21 | * Within this effect we won't dispatch any other store actions. 22 | */ 23 | private showSnackbarEffect$: Observable = createEffect((): Observable => 24 | this.actions$.pipe( 25 | ofType(errorActions.showSnackbar), 26 | map((action): ServerErrorInterface => action.error), 27 | map((error: ServerErrorInterface): void => { 28 | this.snackbarService 29 | .error(error) 30 | .finally(); 31 | }), 32 | ), 33 | { dispatch: false }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /doc/TOOLS.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This document contains information about _common_ tools that are included to 4 | this application. All these tools are available inside application docker 5 | container. 6 | 7 | ## Table of Contents 8 | 9 | * [What is this?](#what-is-this) 10 | * [Table of Contents](#table-of-contents) 11 | * [npm-check-updates](#npm-check-updates-table-of-contents) 12 | * [mversion](#mversion-table-of-contents) 13 | 14 | ## npm-check-updates [ᐞ](#table-of-contents) 15 | 16 | npm-check-updates upgrades your package.json dependencies to the latest 17 | versions, ignoring specified versions. 18 | 19 | npm-check-updates maintains your existing semantic versioning policies, i.e., 20 | it will upgrade `"express": "^4.0.0"` to `"express": "^5.0.0"`. 21 | 22 | * [Website](https://github.com/tjunnone/npm-check-updates) 23 | 24 | ## mversion [ᐞ](#table-of-contents) 25 | 26 | A cross packaging manager module version handler/bumper. Imitates _npm version_ 27 | to also work on other packaging files. For those times you either have 28 | multiple packaging files (like `bower.json`, `component.json`, `manifest.json`) 29 | or just not a `package.json` file. mversion can easily bump your version and 30 | optionally commit and create a tag. 31 | 32 | * [Website](https://github.com/mikaelbr/mversion) 33 | 34 | --- 35 | 36 | [Back to resources index](README.md) - [Back to main README.md](../README.md) 37 | -------------------------------------------------------------------------------- /docker/ssl/rootCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEPTCCAyWgAwIBAgIUOXCXN0tj+bcg84KhbVHKijZ6P9EwDQYJKoZIhvcNAQEL 3 | BQAwgawxCzAJBgNVBAYTAkZJMRMwEQYDVQQIDApTb21lLVN0YXRlMTkwNwYDVQQK 4 | DDBodHRwczovL2dpdGh1Yi5jb20vdGFybGVwcC9hbmd1bGFyLW5ncngtZnJvbnRl 5 | bmQxOTA3BgNVBAsMMGh0dHBzOi8vZ2l0aHViLmNvbS90YXJsZXBwL2FuZ3VsYXIt 6 | bmdyeC1mcm9udGVuZDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIzMDMxMTExMTQ0 7 | NloYDzIwNTAwNzI3MTExNDQ2WjCBrDELMAkGA1UEBhMCRkkxEzARBgNVBAgMClNv 8 | bWUtU3RhdGUxOTA3BgNVBAoMMGh0dHBzOi8vZ2l0aHViLmNvbS90YXJsZXBwL2Fu 9 | Z3VsYXItbmdyeC1mcm9udGVuZDE5MDcGA1UECwwwaHR0cHM6Ly9naXRodWIuY29t 10 | L3RhcmxlcHAvYW5ndWxhci1uZ3J4LWZyb250ZW5kMRIwEAYDVQQDDAlsb2NhbGhv 11 | c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWbTynjvNbSvY4WtTg 12 | CHykmuyeLSiJXhRudwIexgUBvI3gt8olGaUOZxNL4tn2l8ySbig9FYKVOtmr+cVz 13 | kELMocwCe1oaPtwRRV+9a+/39FzPpI9cZXe43Qv+6ULLET30ju5+GFV2j4VoNMv5 14 | dFcB8mb/QznYT2eWiH/MXsVT4SFiqHl7uMDa421tJ96bxodGjr2RUskKnDUD1HxU 15 | cw9KO3zjhtwRsF+FT0DS1V/hqNuCJ2T/CLP0F49HdZT0Mp5ZEywoN6d5FJhKBb1x 16 | Sc1gE9jEfooGt/ji9zhyYWvcJZ/NfQp0GPxy+mrv2stal7jr04oBbE2GFdIYIVZq 17 | bZtrAgMBAAGjUzBRMB0GA1UdDgQWBBSMH04buIx3Y2NlU39Ub+0bzC6xQTAfBgNV 18 | HSMEGDAWgBSMH04buIx3Y2NlU39Ub+0bzC6xQTAPBgNVHRMBAf8EBTADAQH/MA0G 19 | CSqGSIb3DQEBCwUAA4IBAQBV1j2oBy44+MzJKZ2hScabp383nTNuVK6FsfvmZEHL 20 | e2As83Kps9LuH3pnMxlimB/hK5zrdWs3vq7jE1usnUJQtHTA3zhc5/DhN0GyBnq+ 21 | q7A0N0YIpGexDPldgnjETD7XmYQCNJ7i6i0PI3Ir0ruccd+P1vC458meUOh7cPHl 22 | AWdu4yw0yOdfNzTox/0nnTaXN5hPFQQm7u9Nj2ZcrTUVbBrRMSLR7BnP0YlyI85I 23 | X3rJOnPqxzaY/iCjsz1x7CQm5xV2z3Qd5cIu5kNQBWq09zkr2ounBEUMkl7Q8AKA 24 | qo5jkiOlJPvb/p+Zdu5Spjn7GTkuoVp388RbbIiq2alp 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | if ($http_x_forwarded_proto = "http") { 6 | return 301 https://$host$request_uri; 7 | } 8 | 9 | location / { 10 | location = /assets/version.json { 11 | expires -1; 12 | proxy_no_cache 1; 13 | proxy_cache_bypass 1; 14 | } 15 | 16 | location = /assets/i18n/en.json { 17 | expires -1; 18 | proxy_no_cache 1; 19 | proxy_cache_bypass 1; 20 | } 21 | 22 | location = /assets/i18n/fi.json { 23 | expires -1; 24 | proxy_no_cache 1; 25 | proxy_cache_bypass 1; 26 | } 27 | 28 | root /usr/share/nginx/html; 29 | index index.html; 30 | try_files $uri$args $uri$args/ $uri $uri/ /index.html =404; 31 | } 32 | 33 | gzip on; 34 | gzip_disable "msie6"; 35 | gzip_vary on; 36 | gzip_proxied any; 37 | gzip_comp_level 6; 38 | gzip_buffers 16 8k; 39 | gzip_http_version 1.1; 40 | gzip_min_length 256; 41 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; 42 | 43 | server_tokens off; 44 | disable_symlinks off; 45 | 46 | error_log /var/log/nginx/app_error.log; 47 | access_log /var/log/nginx/app_access.log; 48 | 49 | error_page 500 502 503 504 /50x.html; 50 | location = /50x.html { 51 | root /usr/share/nginx/html; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/shared/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | 3 | import { AutoFocusDirective } from 'src/app/shared/directives/auto-focus.directive'; 4 | import { HasAllRolesDirective } from 'src/app/shared/directives/has-all-roles.directive'; 5 | import { HasNotAllRolesDirective } from 'src/app/shared/directives/has-not-all-roles.directive'; 6 | import { HasNotRoleDirective } from 'src/app/shared/directives/has-not-role.directive'; 7 | import { HasNotSomeRoleDirective } from 'src/app/shared/directives/has-not-some-role.directive'; 8 | import { HasRoleDirective } from 'src/app/shared/directives/has-role.directive'; 9 | import { HasSomeRoleDirective } from 'src/app/shared/directives/has-some-role.directive'; 10 | import { IsLoggedInDirective } from 'src/app/shared/directives/is-logged-in.directive'; 11 | 12 | export * from 'src/app/shared/directives/auto-focus.directive'; 13 | export * from 'src/app/shared/directives/has-all-roles.directive'; 14 | export * from 'src/app/shared/directives/has-not-all-roles.directive'; 15 | export * from 'src/app/shared/directives/has-not-role.directive'; 16 | export * from 'src/app/shared/directives/has-not-some-role.directive'; 17 | export * from 'src/app/shared/directives/has-role.directive'; 18 | export * from 'src/app/shared/directives/has-some-role.directive'; 19 | export * from 'src/app/shared/directives/is-logged-in.directive'; 20 | 21 | export const directives: Array> = [ 22 | AutoFocusDirective, 23 | HasAllRolesDirective, 24 | HasNotAllRolesDirective, 25 | HasNotRoleDirective, 26 | HasNotSomeRoleDirective, 27 | HasRoleDirective, 28 | HasSomeRoleDirective, 29 | IsLoggedInDirective, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/app/store/layout/layout.state.ts: -------------------------------------------------------------------------------- 1 | import { Device, Language, Locale, Theme, Viewport } from 'src/app/shared/enums'; 2 | 3 | /** 4 | * Interface definition for our layout store contents. 5 | * 6 | * theme 7 | * Current theme that is used within application this will affect the 8 | * overall look of your application 9 | * 10 | * language 11 | * Current language that is used within application, this will affect all 12 | * messages within our application. 13 | * 14 | * locale 15 | * Current locale that is used within application, this will affect to 16 | * time, date, datetime, number and currency formatting. 17 | * 18 | * timezone 19 | * Current timezone that is used within application, this will affect to 20 | * time, date and datetime formatting. 21 | * 22 | * viewport 23 | * Current viewport definition of user browser who is using application. 24 | * 25 | * device 26 | * Current device definition of user browser who is using application. 27 | * 28 | * isDesktop 29 | * Is user using this application with desktop device or not. 30 | * 31 | * isTablet 32 | * Is user using this application with tablet device or not. 33 | * 34 | * isMobile 35 | * Is user using this application with mobile device or not. 36 | * 37 | * anchor 38 | * HTML anchor definition where we want to scroll user browser. 39 | */ 40 | export interface LayoutState { 41 | theme: Theme; 42 | language: Language; 43 | locale: Locale; 44 | timezone: string; 45 | viewport: Viewport; 46 | device: Device; 47 | isDesktop: boolean; 48 | isTablet: boolean; 49 | isMobile: boolean; 50 | anchor: string|null; 51 | } 52 | -------------------------------------------------------------------------------- /src/app/store/error/error.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | 3 | import { ServerErrorInterface } from 'src/app/shared/interfaces'; 4 | import { ErrorType } from 'src/app/store/store.types'; 5 | 6 | /** 7 | * Error store actions definitions, each of these actions will change the 8 | * state of this store. 9 | * 10 | * Simple usage example; 11 | * 12 | * public constructor(private store: Store) { } 13 | * 14 | * public ngOnInit(): void { 15 | * const error: ServerErrorInterface = { 16 | * code: 0, 17 | * message: 'Some message', 18 | * status: 0, 19 | * statusText: '', 20 | * }; 21 | * 22 | * // Dispatch action to show error in snackbar 23 | * this.store.dispatch(errorActions.showSnackbar({ error })); 24 | * } 25 | */ 26 | 27 | /** 28 | * Action to trigger snackbar with specified error. Usually this action 29 | * is dispatched from application `ErrorInterceptor` - if and when some 30 | * HTTP error occurred. But there is no reason for you not to use this 31 | * if there is some special need to show some error message easily. 32 | */ 33 | const showSnackbar = createAction(ErrorType.SHOW_SNACKBAR, props<{ error: ServerErrorInterface }>()); 34 | 35 | /** 36 | * Action to clear snackbar error, this is called from snackbar service 37 | * when user dismiss that error snackbar - so this action is basically 38 | * just for internal usage. 39 | * 40 | * @internal 41 | */ 42 | const clearSnackbar = createAction(ErrorType.CLEAR_SNACKBAR); 43 | 44 | // Export all store actions, so that those can be used easily. 45 | export const errorActions = { 46 | showSnackbar, 47 | clearSnackbar, 48 | }; 49 | -------------------------------------------------------------------------------- /doc/USAGE_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This document contains basic checklist what _you_ need to do if you're going to 4 | use this template as in base of your own application. 5 | 6 | ## Table of Contents 7 | 8 | * [What is this?](#what-is-this) 9 | * [Table of Contents](#table-of-contents) 10 | * [Checklist](#checklist-table-of-contents) 11 | 12 | ## Checklist [ᐞ](#table-of-contents) 13 | 14 | Below you have _basic_ checklist that **_you need to go through_** after you have 15 | started to use this template. 16 | 17 | * [ ] Check that [LICENSE](../LICENSE) matches to your needs and change it if 18 | needed. 19 | * [ ] Check that [README.md](../README.md) contains only things related to your 20 | application. 21 | * [ ] Update [package.json](../package.json) to match with your application. 22 | Below you see the parts that you should check/update; 23 | * [ ] Common properties; `name`, `description`, `author`, `license`, 24 | `homepage`, `keywords` and `bugs` 25 | * [ ] Update [compose.yaml](../compose.yaml) to match with your 26 | application. Below you see the parts that you should check/update; 27 | * [ ] Change `container_name` to match your application 28 | * [ ] GitHub Actions - This application is using GitHub Actions to run multiple 29 | jobs to check application code. 30 | * [ ] [main.yml](../.github/workflows/main.yml) - Check file contents and 31 | modify it for your needs. 32 | * [ ] [vulnerability-scan.yml](../.github/workflows/vulnerability-scan.yml) - 33 | Check file contents and modify it for your needs. 34 | * [ ] Delete this file. 35 | 36 | --- 37 | 38 | [Back to resources index](README.md) - [Back to main README.md](../README.md) 39 | -------------------------------------------------------------------------------- /docker/ssl/README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This folder contains all docker configuration for local development 4 | environment. 5 | 6 | ## Table of Contents 7 | 8 | * [What is this?](#what-is-this) 9 | * [Table of Contents](#table-of-contents) 10 | * [Selfsigned SSL certificates](#selfsigned-ssl-certificates-ᐞ) 11 | 12 | ## Selfsigned SSL certificates [ᐞ](#table-of-contents) 13 | 14 | Note that this directory contains following SSL certification files: 15 | 16 | * [rootCA.key](rootCA.key) 17 | * [rootCA.pem](rootCA.pem) 18 | * [tls.crt](tls.crt) 19 | * [tls.csr](tls.csr) 20 | * [tls.key](tls.key) 21 | 22 | And these are just for _local_ development environment and these should **not** 23 | be used in _production_ environment. These certificates are valid until 2050, 24 | so I think that is long enough - and if not I'll update those certificates. 25 | 26 | Because application is running on `https` by default now, you will see 27 | security issue on your browser when you access `https://localhost:4200` url. 28 | You see this issue because of these selfsigned certificates. To solve this 29 | issue you've basically two choices: 30 | 31 | 1. Just ignore that security issue (easy way) 32 | 2. Import that [rootCA.pem](rootCA.pem) to your browser as a trusted root 33 | certificate (proper way) 34 | 35 | For that second option see eg. 36 | [this](https://dgu2000.medium.com/working-with-self-signed-certificates-in-chrome-walkthrough-edition-a238486e6858) 37 | article - Specially that `Step 4: Adding CA as trusted to Chrome` part. 38 | 39 | Also [this](https://www.pico.net/kb/how-do-you-get-chrome-to-accept-a-self-signed-certificate/) 40 | article should help you with that process. 41 | 42 | --- 43 | 44 | [Back to main README.md](../../README.md) 45 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | // Base font families 4 | $font-family: 'Roboto', 'Helvetica Neue', sans-serif; 5 | $font-family-monospace: 'Monaco', monospace; 6 | 7 | // Paddings & Margins 8 | $spacing: 32px; 9 | $spacing-half: math.div($spacing, 2); 10 | $spacing-quarter: math.div($spacing, 4); 11 | $spacing-half-quarter: math.div($spacing-quarter, 2); 12 | $spacing-1-5x: $spacing * 1.5; 13 | $spacing-2x: $spacing * 2; 14 | 15 | // Positions 16 | $positions: (top, right, bottom, left); 17 | $orientations: (vertical, horizontal); 18 | 19 | // Sizes 20 | $sizes: ( 21 | '': $spacing, 22 | '-half': $spacing-half, 23 | '-quarter': $spacing-quarter, 24 | '-half-quarter': $spacing-half-quarter, 25 | '-1-5x': $spacing-1-5x, 26 | '-2x': $spacing-2x, 27 | ); 28 | 29 | // Opacities 30 | $opacities: ( 31 | '0': '0', 32 | '7': '.07', 33 | '17': '.17', 34 | '27': '.27', 35 | '37': '.37', 36 | '47': '.47', 37 | '57': '.57', 38 | '67': '.67', 39 | '77': '.77', 40 | '87': '.87', 41 | '97': '.97', 42 | '100': '1', 43 | ); 44 | 45 | // Degrees 46 | $degrees: ( 47 | '45': '45deg', 48 | '90': '90deg', 49 | '135': '135deg', 50 | '180': '180deg', 51 | '225': '225deg', 52 | '270': '270deg', 53 | '315': '315deg' 54 | ); 55 | 56 | // Font-sizes 57 | $font-size-normal: 16px; 58 | $font-size-list: 13px; 59 | $font-size-small: 12px; 60 | 61 | // Border radius 62 | $border-radius: math.div($spacing, 4); 63 | 64 | // Sizes 65 | $header-height: 64px; 66 | 67 | // Animations 68 | $interval: .4s; 69 | $interval-short: math.div($interval, 2); 70 | 71 | // Used hue definitions 72 | $light-hue-primary: 500; 73 | $light-hue-accent: 500; 74 | $light-hue-warn: 500; 75 | $dark-hue-primary: 500; 76 | $dark-hue-accent: 500; 77 | $dark-hue-warn: 500; 78 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard-scss" 4 | ], 5 | "plugins": [ 6 | "stylelint-scss" 7 | ], 8 | "rules": { 9 | "max-nesting-depth": 4, 10 | "selector-max-compound-selectors": 4, 11 | "selector-pseudo-element-no-unknown": [ 12 | true, 13 | { 14 | "ignorePseudoElements": [ 15 | "ng-deep" 16 | ] 17 | } 18 | ], 19 | "selector-type-no-unknown": [ 20 | true, 21 | { 22 | "ignoreTypes": [ 23 | "app-root" 24 | ] 25 | } 26 | ], 27 | "at-rule-no-unknown": [ 28 | true, 29 | { 30 | "ignoreAtRules": [ 31 | "at-root", 32 | "content", 33 | "each", 34 | "else", 35 | "error", 36 | "for", 37 | "function", 38 | "include", 39 | "if", 40 | "mixin", 41 | "return", 42 | "warn", 43 | "use" 44 | ] 45 | } 46 | ], 47 | "scss/at-rule-no-unknown": [ 48 | true, 49 | { 50 | "ignoreAtRules": [ 51 | "at-root", 52 | "content", 53 | "each", 54 | "else", 55 | "error", 56 | "for", 57 | "function", 58 | "include", 59 | "if", 60 | "mixin", 61 | "return", 62 | "warn", 63 | "use" 64 | ] 65 | } 66 | ], 67 | "alpha-value-notation": "number", 68 | "color-function-notation": "legacy", 69 | "no-empty-source": null, 70 | "selector-class-pattern": null, 71 | "selector-max-type": null, 72 | "scss/dollar-variable-pattern": null, 73 | "scss/at-function-pattern": null, 74 | "scss/at-mixin-pattern": null 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 8 * * 5' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v6.0.0 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v4 30 | # Override language selection by uncommenting this and choosing your languages 31 | # with: 32 | # languages: go, javascript, csharp, python, cpp, java 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v4 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v4 52 | -------------------------------------------------------------------------------- /docker/ssl/tls.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3UiOX3zdQ5QgV 3 | VOVYs0zFXCLeOnvbEBH3cp3hwhkDaaGQyYJ6SrTBA86eNK8kP+f2Dod9HK303UAJ 4 | dYg9ahoAFJs7LDcGY9HXHdCxA6+bCr05MVFJhLZLA6XLPX3vbEYx3AIu7aHkvrAt 5 | lurutg+/tmPcGAuBZXTXvWoVHU6uyV0LmxKG/gLqGdNfYrQ3sLzqTTNreWV0Hsjt 6 | 4aTFOe2w1EVU7YtSiT6N+maThQLx0hKYZaqkzcJqED/VaiXiefiiHBlfllGuWgYY 7 | hryT8WSIU58I9jv9KKKpLHwcq7g38PocO7tGip5QMlaDkFJx5z1blFpuntW354rF 8 | nAcup5vpAgMBAAECggEACVNuaB187OqZ3Kvy9u44K5oiU/fZsS1CHLqB+agsKZZI 9 | F9uIfbwrMd6FtlqrxkZSAJyAnAbHert+tHx8L9Oj+vkRZJM9QV/2nuDvavHrDxIe 10 | yjgOEJVlyK8o0gwQygtSDttIaSbBo6dnwwn4bahpaLFYpyu5+h01Mnx6VxSkmhOi 11 | UW8zclt0k25YtrhOqg58FAuqoJFT2CN+DUB2Z9R6qz7Fc8K+6Qg2wd+Ssc1ailq3 12 | AWkyTsvUllOBtUpCa80G0mMEUo/o9BENZNdlp1y8ZHDlHpKGXQT6J36ZIlMY3p+q 13 | eb2I7GeAqy/qq4RepFthZo3qBr6eKi+5a6FfFUV0+QKBgQDelaS/Siw+iaCnkoGM 14 | sbbic22ouO46hgO/QtRWKPEOxytFYJonxb838J+Lj+e5Q2iQRR4gruCspoYdvslz 15 | 8Gz2BZLEMxvRBP8Fn3t0qCfgkTnG+7nzqGlUUOz2dC/GbDFAauDgtiP1i2Seggf8 16 | mdPu+L75amAfrRF1dAtcr1tgXwKBgQDS14QAf2Yyav+vybrPMWy2fQkbfstT+V36 17 | tv2rWAguBacaeyT1nIU+MzaRzz5bDpqL/ZKAl+5cZMe2OLj7MYRwdWOADUk+eZhR 18 | Qii6bOi1ek3IvFTvcw0TVoBHsui7ws0AYVgxLotu+UeCm3uTrTpU10qpTBDgrD8A 19 | VES/fKVItwKBgEAsZs5cBTuWlIIbxEwbJL5PR0uA3fZzkvr+upeIwE9hbwFWo7tZ 20 | 0RLAxk8Hk2ifnbMSqmgD43UDoeFLk2tg4xlfa/wzPA7cYOZvH7n3jV+6nyzWgLtW 21 | T9/mYRuBThDoOp728ZT4DpAJu13T4ZebhjX885qXZ9VaIZF6fpgfWroDAoGAWYOG 22 | V2+/Q8iJpWhQF4c0UIXlR/cc/Bny/G6UZBqL3n4n4+ZEWsKJD09O67jBEIeytZyC 23 | hnX6jN98qrCKWJs9T0GHbOY3In8dW+JqyDtU0TeLrp3GsaJ2+q7O56HdjVm3D2JE 24 | vxyztfm/koPQg3IYQczltdrFj52RYeJlDactxSECgYEAo4cAaDdVkqKdXtL7Bqv3 25 | sL/6+Jf6tl7+92iUpS0VMOMqLNBynXyLJ4ji3lwu7bHQyX13BiaeCLYdxe+y4DPq 26 | 1t+UKsXcID+yxZFV6I5hnX0cs4VnmvGZOo7wRCiaSE8wCa9bI1koBwDHq2aoMrdB 27 | WUPFcIYkXl32vk+j1AUV0M8= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/app/shared/interceptors/accept-language.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 2 | import { Injectable, inject } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { Language } from 'src/app/shared/enums'; 7 | import { ConfigurationService } from 'src/app/shared/services'; 8 | import { layoutSelectors } from 'src/app/store'; 9 | 10 | @Injectable() 11 | export class AcceptLanguageInterceptor implements HttpInterceptor { 12 | private language: Language = Language.DEFAULT; 13 | private readonly store: Store = inject(Store); 14 | 15 | /** 16 | * Constructor of the class, where we subscribe to the store to get user 17 | * selected language. 18 | */ 19 | public constructor() { 20 | this.store.select(layoutSelectors.selectLanguage).subscribe((language: Language): Language => this.language = language); 21 | } 22 | 23 | /** 24 | * Identifies and handles a given HTTP request. 25 | * 26 | * Within this we add `Accept-Language` header with user selected language to 27 | * all requests that are made against our backend - so that backend knows 28 | * which language to use within that request. 29 | */ 30 | public intercept(httpRequest: HttpRequest, next: HttpHandler): Observable> { 31 | if (httpRequest.url.includes(new URL(ConfigurationService.configuration.apiUrl).host)) { 32 | const modified = httpRequest.clone({ 33 | setHeaders: { 34 | // eslint-disable-next-line @typescript-eslint/naming-convention 35 | 'Accept-Language': this.language, 36 | }, 37 | }); 38 | 39 | return next.handle(modified); 40 | } 41 | 42 | return next.handle(httpRequest); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
5 | 53 |
54 | -------------------------------------------------------------------------------- /src/app/shared/directives/has-role.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { authenticationSelectors } from 'src/app/store'; 7 | 8 | @Directive({ 9 | selector: '[appHasRole]', 10 | }) 11 | 12 | export class HasRoleDirective implements OnInit, OnDestroy { 13 | @Input('appHasRole') public role: Role|string = ''; 14 | 15 | private readonly templateRef: TemplateRef = inject>(TemplateRef); 16 | private readonly container: ViewContainerRef = inject(ViewContainerRef); 17 | private readonly store: Store = inject(Store); 18 | private readonly subscription: Subscription = new Subscription(); 19 | 20 | /** 21 | * A callback method that is invoked immediately after the default change 22 | * detector has checked the directive's data-bound properties for the first 23 | * time, and before any of the view or content children have been checked. 24 | * It is invoked only once when the directive is instantiated. 25 | */ 26 | public ngOnInit(): void { 27 | this.subscription.add( 28 | this.store.select(authenticationSelectors.selectHasRole(this.role)).subscribe( 29 | (hasRole: boolean): void => { 30 | if (hasRole) { 31 | this.container.createEmbeddedView(this.templateRef); 32 | 33 | return; 34 | } 35 | 36 | this.container.clear(); 37 | }, 38 | ), 39 | ); 40 | } 41 | 42 | /** 43 | * A callback method that performs custom clean-up, invoked immediately 44 | * before a directive, pipe, or service instance is destroyed. 45 | */ 46 | public ngOnDestroy(): void { 47 | this.subscription.unsubscribe(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/utils/ngrx.utils.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, MemoizedSelector } from '@ngrx/store'; 2 | 3 | import { ServerErrorInterface } from 'src/app/shared/interfaces'; 4 | 5 | /** 6 | * This file contains set of helpers for NgRX usage. 7 | * 8 | * If you spot out some generic (and repeating) code in this application that 9 | * is related to NgRx stuff please refactor those parts and add new helper(s) 10 | * here. 11 | */ 12 | 13 | // Helper selector for any boolean value from specified feature state 14 | export const selectBooleanValue = 15 | (selector: MemoizedSelector, property: string): MemoizedSelector => 16 | createSelector(selector, (state: T2): boolean => getValue(state, property)); 17 | 18 | // Helper selector for any string value from specified feature state 19 | export const selectStringValue = 20 | (selector: MemoizedSelector, property: string): MemoizedSelector => 21 | createSelector(selector, (state: T2): string => getValue(state, property)); 22 | 23 | // Helper selector for `ServerErrorInterface` value from specified feature state 24 | export const selectServerErrorValue = 25 | (selector: MemoizedSelector, property: string): MemoizedSelector => 26 | createSelector(selector, (state: T2): ServerErrorInterface|null => 27 | getValue(state, property)); 28 | 29 | const getValue = (state: T1, property: string): T2 => { 30 | if (!Object.keys(state).includes(property)) { 31 | throw new Error(`Property '${property}' is not defined in state ${JSON.stringify(state)}`); 32 | } 33 | 34 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 35 | // @ts-ignore 36 | return state[property]; 37 | }; 38 | -------------------------------------------------------------------------------- /src/app/shared/directives/has-not-role.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { authenticationSelectors } from 'src/app/store'; 7 | 8 | @Directive({ 9 | selector: '[appHasNotRole]', 10 | }) 11 | 12 | export class HasNotRoleDirective implements OnInit, OnDestroy { 13 | @Input('appHasNotRole') public role: Role|string = ''; 14 | 15 | private readonly templateRef: TemplateRef = inject>(TemplateRef); 16 | private readonly container: ViewContainerRef = inject(ViewContainerRef); 17 | private readonly store: Store = inject(Store); 18 | private readonly subscription: Subscription = new Subscription(); 19 | 20 | /** 21 | * A callback method that is invoked immediately after the default change 22 | * detector has checked the directive's data-bound properties for the first 23 | * time, and before any of the view or content children have been checked. 24 | * It is invoked only once when the directive is instantiated. 25 | */ 26 | public ngOnInit(): void { 27 | this.subscription.add( 28 | this.store.select(authenticationSelectors.selectHasRole(this.role)).subscribe( 29 | (hasRole: boolean): void => { 30 | if (!hasRole) { 31 | this.container.createEmbeddedView(this.templateRef); 32 | 33 | return; 34 | } 35 | 36 | this.container.clear(); 37 | }, 38 | ), 39 | ); 40 | } 41 | 42 | /** 43 | * A callback method that performs custom clean-up, invoked immediately 44 | * before a directive, pipe, or service instance is destroyed. 45 | */ 46 | public ngOnDestroy(): void { 47 | this.subscription.unsubscribe(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/store/version/version.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { selectBooleanValue, selectServerErrorValue } from 'src/app/shared/utils'; 4 | import { VersionState } from 'src/app/store'; 5 | 6 | /** 7 | * Selectors for `VersionState` store. 8 | * 9 | * Simple usage example; 10 | * 11 | * public constructor(private store: Store) { 12 | * this.versionFrontend$ = this.store.select(versionSelectors.selectFrontendVersion); 13 | * this.versionBackend$ = this.store.select(versionSelectors.selectBackendVersion); 14 | * } 15 | */ 16 | 17 | // Feature selector for `version` store. 18 | const selectFeature = createFeatureSelector('version'); 19 | 20 | // Common selectors for this store 21 | const selectFrontendVersion = createSelector(selectFeature, (state: VersionState): string => state.frontend); 22 | const selectBackendVersion = createSelector(selectFeature, (state: VersionState): string => state.backend); 23 | const selectIsLoadingFrontendVersion = selectBooleanValue(selectFeature, 'isLoadingFrontend'); 24 | const selectIsLoadingBackendVersion = selectBooleanValue(selectFeature, 'isLoadingBackend'); 25 | 26 | // Aware state selectors 27 | const selectError = selectServerErrorValue(selectFeature, 'error'); 28 | 29 | // Selector for frontend/backend version loading state. 30 | const selectIsLoading = createSelector( 31 | selectIsLoadingFrontendVersion, 32 | selectIsLoadingBackendVersion, 33 | (isLoadingFrontend: boolean, isLoadingBackend: boolean): boolean => isLoadingFrontend || isLoadingBackend, 34 | ); 35 | 36 | // Export all store selectors, so that those can be used easily. 37 | export const versionSelectors = { 38 | selectFrontendVersion, 39 | selectBackendVersion, 40 | selectIsLoadingFrontendVersion, 41 | selectIsLoadingBackendVersion, 42 | selectIsLoading, 43 | selectError, 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/shared/directives/has-all-roles.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { authenticationSelectors } from 'src/app/store'; 7 | 8 | @Directive({ 9 | selector: '[appHasAllRoles]', 10 | }) 11 | 12 | export class HasAllRolesDirective implements OnInit, OnDestroy { 13 | @Input('appHasAllRoles') public roles: Array = []; 14 | 15 | private readonly templateRef: TemplateRef = inject>(TemplateRef); 16 | private readonly container: ViewContainerRef = inject(ViewContainerRef); 17 | private readonly store: Store = inject(Store); 18 | private readonly subscription: Subscription = new Subscription(); 19 | 20 | /** 21 | * A callback method that is invoked immediately after the default change 22 | * detector has checked the directive's data-bound properties for the first 23 | * time, and before any of the view or content children have been checked. 24 | * It is invoked only once when the directive is instantiated. 25 | */ 26 | public ngOnInit(): void { 27 | this.subscription.add( 28 | this.store.select(authenticationSelectors.selectHasRoles(this.roles)).subscribe( 29 | (hasRoles: boolean): void => { 30 | if (hasRoles) { 31 | this.container.createEmbeddedView(this.templateRef); 32 | 33 | return; 34 | } 35 | 36 | this.container.clear(); 37 | }, 38 | ), 39 | ); 40 | } 41 | 42 | /** 43 | * A callback method that performs custom clean-up, invoked immediately 44 | * before a directive, pipe, or service instance is destroyed. 45 | */ 46 | public ngOnDestroy(): void { 47 | this.subscription.unsubscribe(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/directives/has-not-all-roles.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { authenticationSelectors } from 'src/app/store'; 7 | 8 | @Directive({ 9 | selector: '[appHasNotAllRoles]', 10 | }) 11 | 12 | export class HasNotAllRolesDirective implements OnInit, OnDestroy { 13 | @Input('appHasNotAllRoles') public role: Array = []; 14 | 15 | private readonly templateRef: TemplateRef = inject>(TemplateRef); 16 | private readonly container: ViewContainerRef = inject(ViewContainerRef); 17 | private readonly store: Store = inject(Store); 18 | private readonly subscription: Subscription = new Subscription(); 19 | 20 | /** 21 | * A callback method that is invoked immediately after the default change 22 | * detector has checked the directive's data-bound properties for the first 23 | * time, and before any of the view or content children have been checked. 24 | * It is invoked only once when the directive is instantiated. 25 | */ 26 | public ngOnInit(): void { 27 | this.subscription.add( 28 | this.store.select(authenticationSelectors.selectHasRoles(this.role)).subscribe( 29 | (hasRoles: boolean): void => { 30 | if (!hasRoles) { 31 | this.container.createEmbeddedView(this.templateRef); 32 | 33 | return; 34 | } 35 | 36 | this.container.clear(); 37 | }, 38 | ), 39 | ); 40 | } 41 | 42 | /** 43 | * A callback method that performs custom clean-up, invoked immediately 44 | * before a directive, pipe, or service instance is destroyed. 45 | */ 46 | public ngOnDestroy(): void { 47 | this.subscription.unsubscribe(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": { 3 | "auth": { 4 | "login": { 5 | "error-password": "This field is required.", 6 | "error-username": "This field is required and must contain at least three (3) characters.", 7 | "password": "Password", 8 | "submit": "Login", 9 | "title": "Login", 10 | "username": "Username" 11 | } 12 | }, 13 | "footer": { 14 | "version": { 15 | "backend": "API version", 16 | "frontend": "Application version" 17 | } 18 | }, 19 | "header": { 20 | "menu": { 21 | "language": { 22 | "en": "English", 23 | "fi": "Suomi", 24 | "title": "Language" 25 | }, 26 | "login": "Login", 27 | "logout": "Logout", 28 | "profile": "Profile", 29 | "theme": "Toggle theme" 30 | }, 31 | "title": "Angular NgRx frontend" 32 | }, 33 | "oops": { 34 | "link": "Back to homepage - let's hope everything works...", 35 | "subtitle": "Damn gerbils have stopped running again! Someone has been dispatched to poke them with a sharp stick.", 36 | "title": "Oops..." 37 | }, 38 | "shared": { 39 | "error-message": { 40 | "not-used-production-environment": "This is only shown if Angular `environment` is not `production` - only for debug purposes." 41 | } 42 | }, 43 | "version-change-dialog": { 44 | "button": "Reload application", 45 | "content": "There is new version of this application available `{{versionNew}}` - it's recommend that you reload this page, to get latest version of the application.", 46 | "title": "New version available" 47 | } 48 | }, 49 | "messages": { 50 | "authentication": { 51 | "login": "You signed in.", 52 | "logout": "You signed out.", 53 | "timeout": "Session timeout" 54 | } 55 | }, 56 | "snackbar": { 57 | "close-button": "Close" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docker/ssl/rootCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIyV0CYYnqeDoCAggA 3 | MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECCLU1ShcPXAFBIIEyOL8D7Bevk77 4 | rnfR3JujSFF6Lxj8k9JIKk/rFTWQTth2FqK19SwyY4oNxIfyj5UcaoY6mby8fYv4 5 | ZqviMebS/aw7gDjQMivMYhNmzD/n3JrZhGjiD3MJN+qMW5mnR60Gxf6LU/pthVhF 6 | OPVDJs9FExzY0a7XiLblQfrXOgS+BjH7WjHtq1rial/ZY2MEv4+zHzTTWF/qP6en 7 | bte99J10dwr7H8sbzCSD/DFHK4dwjEnd2Q/V79SfzLCegzXtCrZFTYkuqhh0NBW3 8 | U/s4Ao4hUAfHYRn//yJrW06rynxSntYJOuIp0FMhhNwlQQjN4Zp82KElAPDS8eew 9 | QK9z6BKsanz/DFI+VyQAeXHEqabPVLZuOBkBY8EaEyAqLf5t+lhJZ4tD9/DeyFwl 10 | yynbpSSiwBai29YNZ4pt+rp64kouGy5ie2R6g1GWoPE6KoYMWZprgC91cYjIJS6j 11 | 1DGovkgYqevkmQ9KNYmlpZ9k0FGZFPKf3jSmNrXeUQtn//BGhkYEHWw6S+20fnE7 12 | apCytCJgTFaAc/+C0mAjD11fjeDur9RcHg8c65fKMvKxX5o/1uojSGrQDUxWF3p4 13 | k+CCNKo8bR5CE7AAztJK1JWar4+dKXKRszSARv2Nkcn/gORc654E1PEsCDdkyx7T 14 | HtflnSer43jnGn8srBJXDRBA00pfrBRSuQKNjyXrZcHPtY9Cp08P/duynhm0qiEv 15 | GXnReUgMUYjxL/4OCyxRaaUBRts8Qkitj20vdbZVKxOHfITPrQ0tMdCacjgbrj+z 16 | l8pegjSotdsJGkz03X4rh5dOrcG+zywrk6JxUgCLU3LaDxi/UEnkfJEE6izv95NJ 17 | dRUjjhQbC3AuNdE8497CbcZ7JQeQMGtWIBW39FrlO3yxzr3vGq97kimoQLt/NzQy 18 | Ah0JkF+MV8F9sW+B74OELOpNsZba9A4y8BcDP6qkzdwxPsGBDWNPb76EjLye6aEz 19 | hdre04Mlh8QrOoVybcY6hSgw3GFPt5R62x5rYOS1NED5OjNGbnUyUMT3KIHWKlyZ 20 | 5xXgljpwSDtyfuQ283ujYKeEhuK8GGeMj3oXol4ikC/csfX3pB58m/t3gAG/PpeJ 21 | Ly1DShUDmJedr48ao61hnp1x+2lKxhSi2CVV/c2FyIqEk3AkO1w92HsmixxG/J5x 22 | DZZfu8pQVY50qJyvXAApeawc4a3cvjhIBkC2bNp0GkPOuG7GoR5UoPctAVvg7M6g 23 | psokoRDz20c5r1znpIUrRbt823B88VgsYA7kRcqFHM5WPw4Y2hODpwIaLIHVeEm+ 24 | NyeaSEe7eIYrH4DgK89c1V8t7ZCWCnyT6mny0r30D5FbHCavlBWzvEKHmjpDCMFp 25 | G2eYK4D9RZQzPQKy6zCDWCWvXgBG2Z9eTSSXdZdvOTs8aMpcLMpo98K4eNzFvddM 26 | a0JRpVUVdnhQZQokerEEsTSfMB3ZvKWpzESvj1B5dJgMFJSsO3HpxF394SY2Dyyk 27 | 3P7jfkP5osN454Cvi4dTnjj65iaNtTPl/kt6oAoS3x+1/Xu7uINF4lZgSvt28Lbv 28 | WvfZrWUUB2ycT4J7MeLoES36KTgG+0kG2VghcBJDpbGVQm3Pz5bYNByNM/VfDBfE 29 | sV5YHpoZPlvOBpiwc31Dxw== 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /src/app/shared/interceptors/http-cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpContextToken, 3 | HttpEvent, 4 | HttpHandler, 5 | HttpInterceptor, 6 | HttpRequest, 7 | HttpResponse, 8 | } from '@angular/common/http'; 9 | import { Injectable, inject } from '@angular/core'; 10 | import { Observable, of, tap } from 'rxjs'; 11 | 12 | import { HttpCacheService } from 'src/app/shared/services/http-cache.service'; 13 | 14 | /** 15 | * If you want to enable cache for specified GET request, you need to add 16 | * following context to your request: 17 | * 18 | * this.http.get(url, { context: new HttpContext().set(cacheable, true) }); 19 | */ 20 | export const cacheable = new HttpContextToken(() => false); 21 | 22 | @Injectable() 23 | export class HttpCacheInterceptor implements HttpInterceptor { 24 | private readonly httpCacheService: HttpCacheService = inject(HttpCacheService); 25 | 26 | public intercept(req: HttpRequest, next: HttpHandler): Observable> { 27 | // Only cache requests that are configured to be cached 28 | if (!req.context.get(cacheable)) { 29 | return next.handle(req); 30 | } 31 | 32 | // Invalidate cache if the request method is not GET and process to next interceptor 33 | if (req.method !== 'GET') { 34 | this.httpCacheService.invalidate(); 35 | 36 | return next.handle(req); 37 | } 38 | 39 | const cachedResponse = this.httpCacheService.get(req.url); 40 | 41 | // Cached response found, so return it directly and skip rest of the interceptors 42 | if (cachedResponse) { 43 | return of(cachedResponse); 44 | } 45 | 46 | // Otherwise, continue to the next interceptor and store response to cache 47 | return next.handle(req) 48 | .pipe( 49 | tap((event: HttpEvent): void => { 50 | if (event instanceof HttpResponse) { 51 | this.httpCacheService.store(req.url, event); 52 | } 53 | }), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docker/ssl/tls.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOTCCBCGgAwIBAgIUEGj8tvMBLMlp5O5CBD79i8eBAhAwDQYJKoZIhvcNAQEL 3 | BQAwgawxCzAJBgNVBAYTAkZJMRMwEQYDVQQIDApTb21lLVN0YXRlMTkwNwYDVQQK 4 | DDBodHRwczovL2dpdGh1Yi5jb20vdGFybGVwcC9hbmd1bGFyLW5ncngtZnJvbnRl 5 | bmQxOTA3BgNVBAsMMGh0dHBzOi8vZ2l0aHViLmNvbS90YXJsZXBwL2FuZ3VsYXIt 6 | bmdyeC1mcm9udGVuZDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIzMDMxMTExMTUw 7 | NFoYDzIwNTAwNzI3MTExNTA0WjCBrDELMAkGA1UEBhMCRkkxEzARBgNVBAgMClNv 8 | bWUtU3RhdGUxOTA3BgNVBAoMMGh0dHBzOi8vZ2l0aHViLmNvbS90YXJsZXBwL2Fu 9 | Z3VsYXItbmdyeC1mcm9udGVuZDE5MDcGA1UECwwwaHR0cHM6Ly9naXRodWIuY29t 10 | L3RhcmxlcHAvYW5ndWxhci1uZ3J4LWZyb250ZW5kMRIwEAYDVQQDDAlsb2NhbGhv 11 | c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3UiOX3zdQ5QgVVOVY 12 | s0zFXCLeOnvbEBH3cp3hwhkDaaGQyYJ6SrTBA86eNK8kP+f2Dod9HK303UAJdYg9 13 | ahoAFJs7LDcGY9HXHdCxA6+bCr05MVFJhLZLA6XLPX3vbEYx3AIu7aHkvrAtluru 14 | tg+/tmPcGAuBZXTXvWoVHU6uyV0LmxKG/gLqGdNfYrQ3sLzqTTNreWV0Hsjt4aTF 15 | Oe2w1EVU7YtSiT6N+maThQLx0hKYZaqkzcJqED/VaiXiefiiHBlfllGuWgYYhryT 16 | 8WSIU58I9jv9KKKpLHwcq7g38PocO7tGip5QMlaDkFJx5z1blFpuntW354rFnAcu 17 | p5vpAgMBAAGjggFNMIIBSTAJBgNVHRMEAjAAMIHsBgNVHSMEgeQwgeGAFIwfThu4 18 | jHdjY2VTf1Rv7RvMLrFBoYGypIGvMIGsMQswCQYDVQQGEwJGSTETMBEGA1UECAwK 19 | U29tZS1TdGF0ZTE5MDcGA1UECgwwaHR0cHM6Ly9naXRodWIuY29tL3RhcmxlcHAv 20 | YW5ndWxhci1uZ3J4LWZyb250ZW5kMTkwNwYDVQQLDDBodHRwczovL2dpdGh1Yi5j 21 | b20vdGFybGVwcC9hbmd1bGFyLW5ncngtZnJvbnRlbmQxEjAQBgNVBAMMCWxvY2Fs 22 | aG9zdIIUOXCXN0tj+bcg84KhbVHKijZ6P9EwCwYDVR0PBAQDAgTwMCEGA1UdEQQa 23 | MBiCCyoubG9jYWxob3N0gglsb2NhbGhvc3QwHQYDVR0OBBYEFNW+Y+fxOs1mss4+ 24 | h+X1aEJD1KY5MA0GCSqGSIb3DQEBCwUAA4IBAQARHPeKlRFR0t5UVoavBhmRlrrG 25 | xeCXj7yHXK14KrwB4H3tuHtdJaiGH8t06JwBv8HdKPDxFtj7jaLx7LJlvwW8dzKz 26 | b3Zx1yPLuYAn6mbPazSQawFfs4nBSNWynaTSX1nBk7c7tHx0ipG5ruoJ9LM6WgVy 27 | 1Py+Wib5m9KgAVB8gM3lQCm8ZTprLNL/KJh37qfMTWo/zBPCqOcbnwJ9cnQYC1jS 28 | UsOtQiZYZpWjrBW98sszIk9hpJQz1E1DmB+xIYpgQI4g/g5CTEs9cOw/6enb/QXb 29 | R6YtfbLTIeukaI2X9oJUk6O1KIi9S03VDVjWyVljA1lkxhcrzN7tuz0LDeDe 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /src/assets/i18n/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": { 3 | "auth": { 4 | "login": { 5 | "error-password": "Pakollinen tieto.", 6 | "error-username": "Pakollinen tieto, syötä vähintään kolme (3) merkkiä.", 7 | "password": "Salasana", 8 | "submit": "Kirjaudu sisään", 9 | "title": "Sisäänkirjautuminen", 10 | "username": "Käyttäjätunnus" 11 | } 12 | }, 13 | "footer": { 14 | "version": { 15 | "backend": "Rajapinnan versio", 16 | "frontend": "Sovelluksen versio" 17 | } 18 | }, 19 | "header": { 20 | "menu": { 21 | "language": { 22 | "en": "English", 23 | "fi": "Suomi", 24 | "title": "Kieli" 25 | }, 26 | "login": "Kirjaudu sisään", 27 | "logout": "Kirjaudu ulos", 28 | "profile": "Profiili", 29 | "theme": "Vaihda teema" 30 | }, 31 | "title": "Angular NgRx frontend" 32 | }, 33 | "oops": { 34 | "link": "Takaisin etusivulle - toivotaan että kaikki toimii...", 35 | "subtitle": "Hiton gerbiilit ovat lakanneet taas juoksemasta! Joku on lähetetty tökkäämään niitä terävällä tikulla.", 36 | "title": "Hupsista..." 37 | }, 38 | "shared": { 39 | "error-message": { 40 | "not-used-production-environment": "Tämä on näkyvissä vain jos Angular `environment` ei ole `production` - vain debug tarkoitukseen." 41 | } 42 | }, 43 | "version-change-dialog": { 44 | "button": "Lataa sovellus uudelleen", 45 | "content": "Sovelluksesta on saatavilla uusi versio `{{versionNew}}` ja on suositeltavaa että lataat tämän sivun uudelleen, jotta saat käyttöösi uusimman version sovelluksesta.", 46 | "title": "Sovelluksen versio päivittynyt" 47 | } 48 | }, 49 | "messages": { 50 | "authentication": { 51 | "login": "Kirjauduit sisään.", 52 | "logout": "Kirjauduit ulos.", 53 | "timeout": "Istunto vanhentunut" 54 | } 55 | }, 56 | "snackbar": { 57 | "close-button": "Sulje" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/shared/directives/is-logged-in.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { authenticationSelectors } from 'src/app/store'; 6 | 7 | /** 8 | * Usage examples; 9 | *
Only for logged in users
10 | *
Only for non logged in users
11 | */ 12 | @Directive({ 13 | selector: '[appIsLoggedIn]', 14 | }) 15 | 16 | export class IsLoggedInDirective implements OnInit, OnDestroy { 17 | @Input('appIsLoggedIn') public required?: boolean|string = true; 18 | 19 | private readonly templateRef: TemplateRef = inject>(TemplateRef); 20 | private readonly container: ViewContainerRef = inject(ViewContainerRef); 21 | private readonly store: Store = inject(Store); 22 | private readonly subscription: Subscription = new Subscription(); 23 | 24 | /** 25 | * A callback method that is invoked immediately after the default change 26 | * detector has checked the directive's data-bound properties for the first 27 | * time, and before any of the view or content children have been checked. 28 | * It is invoked only once when the directive is instantiated. 29 | */ 30 | public ngOnInit(): void { 31 | this.subscription.add( 32 | this.store.select(authenticationSelectors.selectIsLoggedIn).subscribe( 33 | (isLoggedIn: boolean): void => { 34 | if ((isLoggedIn && this.required) 35 | || (!isLoggedIn && !this.required) 36 | ) { 37 | this.container.createEmbeddedView(this.templateRef); 38 | 39 | return; 40 | } 41 | 42 | this.container.clear(); 43 | }, 44 | ), 45 | ); 46 | } 47 | 48 | /** 49 | * A callback method that performs custom clean-up, invoked immediately 50 | * before a directive, pipe, or service instance is destroyed. 51 | */ 52 | public ngOnDestroy(): void { 53 | this.subscription.unsubscribe(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/shared/directives/has-some-role.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { authenticationSelectors } from 'src/app/store'; 7 | 8 | /** 9 | * This directive check if current user has "some" of the defined roles or not. 10 | * Note that if user have at least one of those roles it's enough to display 11 | * that specified DOM element. 12 | * 13 | * Usage example; 14 | *
...
15 | */ 16 | @Directive({ 17 | selector: '[appHasSomeRole]', 18 | }) 19 | 20 | export class HasSomeRoleDirective implements OnInit, OnDestroy { 21 | @Input('appHasSomeRole') public role: Array = []; 22 | 23 | private readonly templateRef: TemplateRef = inject>(TemplateRef); 24 | private readonly container: ViewContainerRef = inject(ViewContainerRef); 25 | private readonly store: Store = inject(Store); 26 | private readonly subscription: Subscription = new Subscription(); 27 | 28 | /** 29 | * A callback method that is invoked immediately after the default change 30 | * detector has checked the directive's data-bound properties for the first 31 | * time, and before any of the view or content children have been checked. 32 | * It is invoked only once when the directive is instantiated. 33 | */ 34 | public ngOnInit(): void { 35 | this.subscription.add( 36 | this.store.select(authenticationSelectors.selectHasSomeRole(this.role)).subscribe( 37 | (hasRole: boolean): void => { 38 | if (hasRole) { 39 | this.container.createEmbeddedView(this.templateRef); 40 | 41 | return; 42 | } 43 | 44 | this.container.clear(); 45 | }, 46 | ), 47 | ); 48 | } 49 | 50 | /** 51 | * A callback method that performs custom clean-up, invoked immediately 52 | * before a directive, pipe, or service instance is destroyed. 53 | */ 54 | public ngOnDestroy(): void { 55 | this.subscription.unsubscribe(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/shared/directives/has-not-some-role.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Role } from 'src/app/auth/enums'; 6 | import { authenticationSelectors } from 'src/app/store'; 7 | 8 | /** 9 | * This directive check if current user has "some" of the defined roles or not. 10 | * Note that if user have at least one of those roles it's enough to not 11 | * display that specified DOM element. 12 | * 13 | * Usage example; 14 | *
...
15 | */ 16 | @Directive({ 17 | selector: '[appHasNotSomeRole]', 18 | }) 19 | 20 | export class HasNotSomeRoleDirective implements OnInit, OnDestroy { 21 | @Input('appHasNotSomeRole') public role: Array = []; 22 | 23 | private readonly templateRef: TemplateRef = inject>(TemplateRef); 24 | private readonly container: ViewContainerRef = inject(ViewContainerRef); 25 | private readonly store: Store = inject(Store); 26 | private readonly subscription: Subscription = new Subscription(); 27 | 28 | /** 29 | * A callback method that is invoked immediately after the default change 30 | * detector has checked the directive's data-bound properties for the first 31 | * time, and before any of the view or content children have been checked. 32 | * It is invoked only once when the directive is instantiated. 33 | */ 34 | public ngOnInit(): void { 35 | this.subscription.add( 36 | this.store.select(authenticationSelectors.selectHasSomeRole(this.role)).subscribe( 37 | (hasRole: boolean): void => { 38 | if (!hasRole) { 39 | this.container.createEmbeddedView(this.templateRef); 40 | 41 | return; 42 | } 43 | 44 | this.container.clear(); 45 | }, 46 | ), 47 | ); 48 | } 49 | 50 | /** 51 | * A callback method that performs custom clean-up, invoked immediately 52 | * before a directive, pipe, or service instance is destroyed. 53 | */ 54 | public ngOnDestroy(): void { 55 | this.subscription.unsubscribe(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/store/version/version.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | 3 | import { ServerErrorInterface } from 'src/app/shared/interfaces'; 4 | import { VersionType } from 'src/app/store/store.types'; 5 | 6 | /** 7 | * Version store actions definitions, that you can dispatch to make 8 | * this store to change is state. 9 | * 10 | * These actions are called automatically within this application main 11 | * component and HTTP interceptor. Most likely you don't need to call 12 | * these actions at all. 13 | */ 14 | 15 | // Common actions for version feature store 16 | const fetchVersions = createAction(VersionType.FETCH_VERSIONS); 17 | const fetchFrontendVersion = createAction(VersionType.FETCH_FRONTEND_VERSION); 18 | const fetchBackendVersion = createAction(VersionType.FETCH_BACKEND_VERSION); 19 | const newBackendVersion = createAction(VersionType.NEW_BACKEND_VERSION, props<{ backendVersion: string }>()); 20 | 21 | /** 22 | * Frontend version success action that is triggered via effects. 23 | * 24 | * @internal 25 | */ 26 | const fetchFrontendVersionSuccess = createAction(VersionType.FETCH_FRONTEND_VERSION_SUCCESS, props<{ version: string }>()); 27 | 28 | /** 29 | * Frontend version failure action that is triggered via effects. 30 | * 31 | * @internal 32 | */ 33 | const fetchFrontendVersionFailure = createAction(VersionType.FETCH_FRONTEND_VERSION_FAILURE, props<{ error: ServerErrorInterface }>()); 34 | 35 | /** 36 | * Backend version success action that is triggered via effects. 37 | * 38 | * @internal 39 | */ 40 | const fetchBackendVersionSuccess = createAction(VersionType.FETCH_BACKEND_VERSION_SUCCESS, props<{ version: string }>()); 41 | 42 | /** 43 | * Backend version failure action that is triggered via effects. 44 | * 45 | * @internal 46 | */ 47 | const fetchBackendVersionFailure = createAction(VersionType.FETCH_BACKEND_VERSION_FAILURE, props<{ error: ServerErrorInterface }>()); 48 | 49 | // Export all store actions, so that those can be used easily. 50 | export const versionActions = { 51 | fetchVersions, 52 | fetchFrontendVersion, 53 | fetchFrontendVersionSuccess, 54 | fetchFrontendVersionFailure, 55 | fetchBackendVersion, 56 | fetchBackendVersionSuccess, 57 | fetchBackendVersionFailure, 58 | newBackendVersion, 59 | }; 60 | -------------------------------------------------------------------------------- /src/app/store/authentication/authentication.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | 3 | import { CredentialsRequestInterface, UserDataInterface, UserProfileInterface } from 'src/app/auth/interfaces'; 4 | import { ServerErrorInterface } from 'src/app/shared/interfaces'; 5 | import { AuthenticationType } from 'src/app/store/store.types'; 6 | 7 | /** 8 | * Authentication store actions definitions, each of these actions will change 9 | * the state of this store. 10 | * 11 | * Simple usage example; 12 | * 13 | * public constructor(private store: Store) { 14 | * this.subscription = new Subscription(); 15 | * } 16 | * 17 | * public ngOnInit(): void { 18 | * // Subscribe to user profile changes 19 | * this.subscriptions 20 | * .add(this.store 21 | * .select(authenticationSelectors.profile) 22 | * .subscribe((profile: UserProfileInterface|null): void => { 23 | * console.log(profile); 24 | * }, 25 | * ); 26 | * 27 | * // Dispatch action to fetch profile data 28 | * this.store.dispatch(authenticationActions.profile()); 29 | * } 30 | * 31 | * public ngOnDestroy(): void { 32 | * this.subscription.unsubscribe(); 33 | * } 34 | */ 35 | 36 | // Common actions for authentication feature store 37 | const login = createAction(AuthenticationType.LOGIN, props<{ credentials: CredentialsRequestInterface }>()); 38 | const loginSuccess = createAction(AuthenticationType.LOGIN_SUCCESS, props<{ userData: UserDataInterface }>()); 39 | const loginFailure = createAction(AuthenticationType.LOGIN_FAILURE, props<{ error: ServerErrorInterface }>()); 40 | const profile = createAction(AuthenticationType.PROFILE); 41 | const profileSuccess = createAction(AuthenticationType.PROFILE_SUCCESS, props<{ profile: UserProfileInterface }>()); 42 | const profileFailure = createAction(AuthenticationType.PROFILE_FAILURE, props<{ error: ServerErrorInterface }>()); 43 | const logout = createAction(AuthenticationType.LOGOUT, props<{ message: string|null }>()); 44 | const resetError = createAction(AuthenticationType.RESET_ERROR); 45 | 46 | // Export all store actions, so that those can be used easily. 47 | export const authenticationActions = { 48 | login, 49 | loginSuccess, 50 | loginFailure, 51 | profile, 52 | profileSuccess, 53 | profileFailure, 54 | logout, 55 | resetError, 56 | }; 57 | -------------------------------------------------------------------------------- /src/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | // Use project variables 4 | @use './variables' as vars; 5 | 6 | @function depth($map) { 7 | $level: 1; 8 | 9 | @each $key, $value in $map { 10 | @if meta.type-of($value) == 'map' { 11 | $level: math.max(depth($value) + 1, $level); 12 | } 13 | } 14 | 15 | @return $level; 16 | } 17 | 18 | /** 19 | * Use like; 20 | * .foo { 21 | * @include debug-map($light-theme); 22 | * 23 | * color: #f00; 24 | * } 25 | * 26 | * And after that just look compiled CSS file 27 | */ 28 | /* stylelint-disable */ 29 | @mixin debug-map($map) { 30 | @at-root { 31 | //noinspection ALL 32 | @debug-map { 33 | __tostring__: inspect($map); 34 | __length__: length($map); 35 | __depth__: depth($map); 36 | __keys__: map-keys($map); 37 | 38 | __properties__ { 39 | @each $key, $value in $map { 40 | #{'(' + type-of($value) + ') ' + $key}: inspect($value); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | /* stylelint-enable */ 47 | 48 | @mixin transition($transition...) { 49 | transition: $transition; 50 | } 51 | 52 | @mixin transform($transform...) { 53 | transform: translateY($transform); 54 | } 55 | 56 | @mixin transition-property($property...) { 57 | transition-property: $property; 58 | } 59 | 60 | @mixin transition-duration($duration...) { 61 | transition-duration: $duration; 62 | } 63 | 64 | @mixin transition-timing-function($timing...) { 65 | transition-timing-function: $timing; 66 | } 67 | 68 | @mixin transition-delay($delay...) { 69 | transition-delay: $delay; 70 | } 71 | 72 | @mixin user-select($type) { 73 | user-select: $type; 74 | } 75 | 76 | @mixin transition-ease-in-out-all() { 77 | @include transition(all vars.$interval ease-in-out); 78 | } 79 | 80 | @mixin scroll-bar() { 81 | &::-webkit-scrollbar { 82 | width: vars.$spacing-quarter; 83 | height: vars.$spacing-quarter; 84 | border-radius: vars.$spacing-half; 85 | } 86 | 87 | ::-webkit-scrollbar:horizontal { 88 | height: vars.$spacing-quarter; 89 | } 90 | 91 | &::-webkit-scrollbar-thumb { 92 | border-radius: vars.$spacing-half; 93 | background: var(--theme-primary-color); 94 | } 95 | 96 | &::-webkit-scrollbar-track { 97 | border-radius: vars.$spacing-half; 98 | background: var(--theme-primary-color-contrast); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/app/shared/services/version.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable, inject } from '@angular/core'; 3 | import { Observable, Observer } from 'rxjs'; 4 | import { take } from 'rxjs/operators'; 5 | 6 | import { ServerErrorInterface, VersionInterface } from 'src/app/shared/interfaces'; 7 | import { ConfigurationService } from 'src/app/shared/services/configuration-service'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class VersionService { 13 | private readonly httpClient: HttpClient = inject(HttpClient); 14 | 15 | /** 16 | * Method to fetch frontend side version information from static JSON file 17 | * that is generated when application is built. So if this file contains 18 | * different version than our current `environment.version` we are sure that 19 | * frontend application has been updated and user hasn't done hard reload 20 | * after that. 21 | */ 22 | public fetchFrontendVersion(): Observable { 23 | const ts = Math.round((new Date()).getTime() / 1000); 24 | 25 | return new Observable((observer: Observer): void => { 26 | this.httpClient 27 | .get(`/assets/version.json?t=${ts}`) 28 | .pipe(take(1)) 29 | .subscribe({ 30 | next: (data: VersionInterface|any): void => observer.next(data.version), 31 | error: (error: ServerErrorInterface): void => observer.error(error), 32 | complete: (): void => observer.complete(), 33 | }); 34 | }); 35 | } 36 | 37 | /** 38 | * Method to fetch backend side version information from specified API 39 | * endpoint. This method is just used once when application is initialized. 40 | * 41 | * After that point we will dispatch backend version changes from each API 42 | * endpoint request via specified HTTP interceptor. 43 | */ 44 | public fetchBackendVersion(): Observable { 45 | const ts = Math.round((new Date()).getTime() / 1000); 46 | 47 | return new Observable((observer: Observer): void => { 48 | this.httpClient 49 | .get(`${ConfigurationService.configuration.apiUrl}/version?t=${ts}`) 50 | .pipe(take(1)) 51 | .subscribe({ 52 | next: (data: VersionInterface|any): void => observer.next(data.version), 53 | error: (error: ServerErrorInterface): void => observer.error(error), 54 | complete: (): void => observer.complete(), 55 | }); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags.ts'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /src/app/auth/guards/base-role.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { Router, UrlTree } from '@angular/router'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { Role } from 'src/app/auth/enums'; 7 | import { RoleGuardMetaDataInterface } from 'src/app/auth/interfaces'; 8 | import { authenticationSelectors } from 'src/app/store'; 9 | 10 | export abstract class BaseRole { 11 | private readonly router: Router = inject(Router); 12 | private readonly store: Store = inject(Store); 13 | 14 | /** 15 | * Helper method to make check if user has certain role or not. This is used 16 | * from following guards: 17 | * - RoleAdminGuard 18 | * - RoleALoggedGuard 19 | * - RoleRootGuard 20 | * - RoleUserGuard 21 | * 22 | * By default, this method will redirect user either to `/` or `/auth/login` 23 | * depending on if user is not logged in or user doesn't have the specified 24 | * role. 25 | * 26 | * You can override this behaviour by setting `data` option to your route 27 | * definition where you can configure following; 28 | * - `redirect`, Redirect if not logged in or no role, defaults to true 29 | * - `routeNotLoggedIn`, Not logged in route, defaults to '/auth/login' 30 | * - `routeNoRole`, No specified role route, defaults to '/' 31 | * 32 | * Simple example about that route configuration; 33 | * 34 | * export const FeatureRoutes: Routes = [ 35 | * { 36 | * path: 'foo', 37 | * canActivate: [ 38 | * () => inject(RoleAdminGuard).canActivate(inject(ActivatedRoute).snapshot), 39 | * ], 40 | * component: FooComponent, 41 | * data: { 42 | * roleGuardMeta: { 43 | * redirect: true, 44 | * routeNotLoggedIn: '/some/route', 45 | * routeNoRole: '/some/another/route', 46 | * }, 47 | * }, 48 | * }, 49 | * ]; 50 | * 51 | * Also note that you don't need to provide all those if you just need to 52 | * change one of those for your needs - and if the defaults are fine for your 53 | * use case then you don't need to specify that `roleGuardMeta` at all. 54 | */ 55 | protected checkRole(role: Role, routeMetaData: RoleGuardMetaDataInterface|null): Observable { 56 | const metaData: RoleGuardMetaDataInterface = { 57 | redirect: true, 58 | routeNotLoggedIn: '/auth/login', 59 | routeNoRole: '/', 60 | ...routeMetaData, 61 | }; 62 | 63 | return this.store.select(authenticationSelectors.selectRoleGuard(role, metaData, this.router)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /doc/DEPENDENCY_UPDATE.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This documentation contains information on how to update package dependencies 4 | within this application. The documentation relies on that you are using 5 | [Docker Engine](https://docs.docker.com/engine/install/) to run this 6 | application. 7 | 8 | ## Table of Contents 9 | 10 | * [What is this?](#what-is-this) 11 | * [Table of Contents](#table-of-contents) 12 | * [Quick steps](#quick-steps-table-of-contents) 13 | * [Major version update](#major-version-update-table-of-contents) 14 | * [Other updates](#other-updates-table-of-contents) 15 | * [External links and resources](#external-links-and-resources-table-of-contents) 16 | 17 | ## Quick steps [ᐞ](#table-of-contents) 18 | 19 | There are a few quick steps below on what you need to do to update the 20 | application package dependencies. 21 | 22 | 1. Get `bash` to the container with `make bash` command 23 | 2. Use `ng update` to check Angular core package updates **and follow 24 | the instructions of this tool to update these packages** 25 | 3. Update rest of the packages, with one of the following commands: 26 | 1. Use `make update` command to use interactive yarn update command to 27 | update all the packages. 28 | 2. Use `ncu` to list remaining packages that can be updated. Then you can 29 | * Change the versions to `package.json` file and restart the application 30 | container - this will trigger `yarn` to install new versions of those 31 | packages and also updates `yarn.lock` file properly. 32 | * Or just use `ncu -u` to automatically update all of those packages and 33 | restart the application container. 34 | 3. Run command `yarn upgrade @` which will update both 35 | `package.json` and `yarn.lock`. After doing this for all needed 36 | packages, restart the application container. 37 | 4. Test that the application works. 38 | 5. Profit ¯\\\_(ツ)_/¯ 39 | 40 | ## Major version update [ᐞ](#table-of-contents) 41 | 42 | When there is a `Major` version update (eg. 12.x.y to 13.0.0) within Angular we 43 | need to check proper instructions from official [Angular Update Guide](https://update.angular.io). 44 | 45 | ## Other updates [ᐞ](#table-of-contents) 46 | 47 | From time to time it's a good idea to update library dependencies as well. You 48 | can do that with following command - that will update `yarn.lock` file. 49 | 50 | ```bash 51 | yarn upgrade 52 | ``` 53 | 54 | ## External links and resources [ᐞ](#table-of-contents) 55 | 56 | * [Angular Update Guide](https://update.angular.io) 57 | * [ng update](https://angular.io/cli/update) 58 | 59 | --- 60 | 61 | [Back to resources index](README.md) - [Back to main README.md](../README.md) 62 | -------------------------------------------------------------------------------- /src/styles/palettes.scss: -------------------------------------------------------------------------------- 1 | // Use eg. http://mcg.mbitson.com/ to create new one for your needs. 2 | 3 | // Accent palette 4 | $app-palette-accent: ( 5 | 50: #fef6ed, 6 | 100: #fce8d3, 7 | 200: #fbd9b5, 8 | 300: #f9c997, 9 | 400: #f7be81, 10 | 500: #f6b26b, 11 | 600: #f5ab63, 12 | 700: #f3a258, 13 | 800: #f2994e, 14 | 900: #ef8a3c, 15 | A100: #fff, 16 | A200: #fff, 17 | A400: #ffe7d5, 18 | A700: #ffd8bc, 19 | contrast: ( 20 | 50: #000, 21 | 100: #000, 22 | 200: #000, 23 | 300: #000, 24 | 400: #000, 25 | 500: #000, 26 | 600: #000, 27 | 700: #000, 28 | 800: #000, 29 | 900: #000, 30 | A100: #000, 31 | A200: #000, 32 | A400: #000, 33 | A700: #000, 34 | ) 35 | ); 36 | 37 | // Grey palette 38 | $app-palette-grey: ( 39 | 50: #e6e6e6, 40 | 100: #c1c1c1, 41 | 200: #979797, 42 | 300: #6d6d6d, 43 | 400: #4e4e4e, 44 | 500: #2f2f2f, 45 | 600: #2a2a2a, 46 | 700: #232323, 47 | 800: #1d1d1d, 48 | 900: #121212, 49 | A100: #ef6b6b, 50 | A200: #e93d3d, 51 | A400: #f30000, 52 | A700: #da0000, 53 | contrast: ( 54 | 50: #000, 55 | 100: #000, 56 | 200: #000, 57 | 300: #fff, 58 | 400: #fff, 59 | 500: #fff, 60 | 600: #fff, 61 | 700: #fff, 62 | 800: #fff, 63 | 900: #fff, 64 | A100: #000, 65 | A200: #fff, 66 | A400: #fff, 67 | A700: #fff, 68 | ) 69 | ); 70 | 71 | // Primary palette 72 | $app-palette-primary: ( 73 | 50: #fee8e7, 74 | 100: #fcc7c3, 75 | 200: #faa19b, 76 | 300: #f77b72, 77 | 400: #f65f54, 78 | 500: #f44336, 79 | 600: #f33d30, 80 | 700: #f13429, 81 | 800: #ef2c22, 82 | 900: #ec1e16, 83 | A100: #fff, 84 | A200: #ffe9e9, 85 | A400: #ffb8b6, 86 | A700: #ff9f9c, 87 | contrast: ( 88 | 50: #000, 89 | 100: #000, 90 | 200: #000, 91 | 300: #000, 92 | 400: #000, 93 | 500: #fff, 94 | 600: #fff, 95 | 700: #fff, 96 | 800: #fff, 97 | 900: #fff, 98 | A100: #000, 99 | A200: #000, 100 | A400: #000, 101 | A700: #000, 102 | ) 103 | ); 104 | 105 | // Warn palette 106 | $app-palette-warn: ( 107 | 50: #f9e0e0, 108 | 100: #f0b3b3, 109 | 200: #e68080, 110 | 300: #db4d4d, 111 | 400: #d42626, 112 | 500: #c00, 113 | 600: #c70000, 114 | 700: #c00000, 115 | 800: #b90000, 116 | 900: #ad0000, 117 | A100: #ffd7d7, 118 | A200: #ffa4a4, 119 | A400: #ff7171, 120 | A700: #ff5858, 121 | contrast: ( 122 | 50: #000, 123 | 100: #000, 124 | 200: #000, 125 | 300: #fff, 126 | 400: #fff, 127 | 500: #fff, 128 | 600: #fff, 129 | 700: #fff, 130 | 800: #fff, 131 | 900: #fff, 132 | A100: #000, 133 | A200: #000, 134 | A400: #000, 135 | A700: #000, 136 | ) 137 | ); 138 | -------------------------------------------------------------------------------- /src/app/store/layout/layout.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { Device, Language, Locale, Theme, Viewport } from 'src/app/shared/enums'; 4 | import { LocalizationInterface } from 'src/app/shared/interfaces'; 5 | import { selectBooleanValue, selectStringValue } from 'src/app/shared/utils'; 6 | import { LayoutState } from 'src/app/store'; 7 | 8 | /** 9 | * Selectors for `LayoutState` store. 10 | * 11 | * Simple usage example; 12 | * 13 | * public constructor(private store: Store) { 14 | * this.viewport$ = this.store.select(layoutSelectors.selectViewport); 15 | * this.device$ = this.store.select(layoutSelectors.selectDevice); 16 | * } 17 | */ 18 | 19 | // Feature selector for `layout` store 20 | const selectFeature = createFeatureSelector('layout'); 21 | 22 | // Common selectors for this store 23 | const selectTheme = createSelector(selectFeature, (state: LayoutState): Theme => state.theme); 24 | const selectLanguage = createSelector(selectFeature, (state: LayoutState): Language => state.language); 25 | const selectLocale = createSelector(selectFeature, (state: LayoutState): Locale => state.locale); 26 | const selectTimezone = selectStringValue(selectFeature, 'timezone'); 27 | const selectViewport = createSelector(selectFeature, (state: LayoutState): Viewport => state.viewport); 28 | const selectDevice = createSelector(selectFeature, (state: LayoutState): Device => state.device); 29 | const selectIsMobile = selectBooleanValue(selectFeature, 'isMobile'); 30 | const selectIsTablet = selectBooleanValue(selectFeature, 'isTablet'); 31 | const selectIsDesktop = selectBooleanValue(selectFeature, 'isDesktop'); 32 | const selectIsDefaultTheme = createSelector(selectTheme, (theme: Theme): boolean => theme === Theme.DEFAULT); 33 | const selectIsDarkTheme = createSelector(selectTheme, (theme: Theme): boolean => theme === Theme.DARK); 34 | const selectIsLightTheme = createSelector(selectTheme, (theme: Theme): boolean => theme === Theme.LIGHT); 35 | 36 | /** 37 | * Selector for `localization` data, which contains; 38 | * - locale 39 | * - language 40 | * - timezone 41 | */ 42 | const selectLocalization = createSelector( 43 | selectLocale, 44 | selectLanguage, 45 | selectTimezone, 46 | (locale: Locale, language: Language, timezone: string): LocalizationInterface => ({ 47 | locale, 48 | language, 49 | timezone, 50 | }), 51 | ); 52 | 53 | // Export all store selectors, so that those can be used easily. 54 | export const layoutSelectors = { 55 | selectTheme, 56 | selectLanguage, 57 | selectLocale, 58 | selectTimezone, 59 | selectViewport, 60 | selectDevice, 61 | selectIsMobile, 62 | selectIsTablet, 63 | selectIsDesktop, 64 | selectIsDefaultTheme, 65 | selectIsDarkTheme, 66 | selectIsLightTheme, 67 | selectLocalization, 68 | }; 69 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular NgRx Frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 97 | 98 | 99 | 100 | 101 | 102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /src/app/auth/guards/base-auth.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { Router, UrlTree } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { map, take } from 'rxjs/operators'; 5 | 6 | import { AuthGuardMetaDataInterface } from 'src/app/auth/interfaces'; 7 | import { AuthenticationService } from 'src/app/auth/services'; 8 | 9 | export abstract class BaseAuth { 10 | private readonly router: Router = inject(Router); 11 | private readonly authenticationService: AuthenticationService = inject(AuthenticationService); 12 | 13 | /** 14 | * Helper method to make check if user needs to be authenticated or not. This 15 | * is used from following guards: 16 | * - AnonymousGuard 17 | * - AuthenticationGuard 18 | * 19 | * By default, this method will redirect user either to `/` or `/auth/login` 20 | * depending on if user needs to be authenticated or not. 21 | * 22 | * You can override this behaviour by setting `data` option to your route 23 | * definition where you can configure following; 24 | * - Redirect if mismatch authentication, defaults to true 25 | * - Authenticated route, defaults to '/' 26 | * - Not authenticated route, defaults to '/auth/login' 27 | * 28 | * Simple example about that route configuration; 29 | * 30 | * export const LoginRoutes: Routes = [ 31 | * { 32 | * path: 'login', 33 | * canActivate: [ 34 | * () => inject(AnonymousGuard).canActivate(inject(ActivatedRoute).snapshot), 35 | * ], 36 | * component: LoginComponent, 37 | * data: { 38 | * authGuardMeta: { 39 | * redirectIfMismatch: true, 40 | * routeAuthenticated: '/some/route', 41 | * routeNotAuthenticated: '/some/another/route', 42 | * }, 43 | * }, 44 | * children: [ 45 | * { 46 | * path: '**', 47 | * redirectTo: 'login', 48 | * }, 49 | * ], 50 | * }, 51 | * ]; 52 | * 53 | * Also note that you don't need to provide all those if you just need to 54 | * change one of those for your needs. 55 | */ 56 | protected makeCheck( 57 | needsToBeAuthenticated: boolean, 58 | routeMetaData: AuthGuardMetaDataInterface|null, 59 | ): Observable { 60 | const metaData: AuthGuardMetaDataInterface = { 61 | redirectIfMismatch: true, 62 | routeAuthenticated: '/', 63 | routeNotAuthenticated: '/auth/login', 64 | ...routeMetaData, 65 | }; 66 | 67 | return this.authenticationService 68 | .isAuthenticated() 69 | .pipe( 70 | take(1), 71 | map((authenticated: boolean): boolean|UrlTree => 72 | (authenticated !== needsToBeAuthenticated && metaData.redirectIfMismatch) 73 | ? this.router.parseUrl(authenticated ? metaData.routeAuthenticated : metaData.routeNotAuthenticated) 74 | : authenticated === needsToBeAuthenticated, 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/shared/pipes/local-number.pipe.ts: -------------------------------------------------------------------------------- 1 | import { formatNumber } from '@angular/common'; 2 | import { OnDestroy, Pipe, PipeTransform, inject } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | import { Locale } from 'src/app/shared/enums'; 7 | import { layoutSelectors } from 'src/app/store'; 8 | 9 | /** 10 | * Locale aware number pipe that uses Angular internal `DecimalPipe` implementation 11 | * within application user specified locale. 12 | * 13 | * Usage; 14 | * {{ someNumber | localNumber [ : digitsInfo [ : locale ] ] }} 15 | * 16 | * This pipes accepts the same pipe arguments than Angular original `DecimalPipe` 17 | * implementation, the only main difference is that user locale is used automatically, 18 | * if you don't pass that to this `localNumber` pipe as you would with that Angular 19 | * core pipe. 20 | * 21 | * Note that this pipe isn't `pure` one, because otherwise we cannot get those 22 | * possible locale changes to work as expected. Internally this pipe is using 23 | * local cache, so that we don't do fire unnecessary function calls on every 24 | * change-detection cycle. 25 | */ 26 | @Pipe({ 27 | name: 'localNumber', 28 | pure: false, 29 | }) 30 | export class LocalNumberPipe implements PipeTransform, OnDestroy { 31 | private locale: Locale = Locale.DEFAULT; 32 | private cachedLocale: Locale = Locale.DEFAULT; 33 | private cachedOutput: string|null = null; 34 | 35 | private readonly store: Store = inject(Store); 36 | private readonly subscriptions: Subscription = new Subscription(); 37 | 38 | /** 39 | * Constructor of the class, where we DI all services that we need to use 40 | * within this component and initialize needed properties. 41 | */ 42 | public constructor() { 43 | // Subscribe to locale changes 44 | this.subscriptions 45 | .add(this.store 46 | .select(layoutSelectors.selectLocale) 47 | .subscribe((locale: Locale): Locale => this.locale = locale), 48 | ); 49 | } 50 | 51 | /** 52 | * A callback method that performs custom clean-up, invoked immediately 53 | * before a directive, pipe, or service instance is destroyed. 54 | */ 55 | public ngOnDestroy(): void { 56 | this.subscriptions.unsubscribe(); 57 | } 58 | 59 | /** 60 | * Angular invokes the `transform` method with the value of a binding as the 61 | * first argument, and any parameters as the second argument in list form. 62 | * 63 | * Note that we use local cache here, so that we don't fire function calls on 64 | * every change-detection cycle. 65 | */ 66 | public transform(value: number|string|null, format?: string, locale?: string): string { 67 | const currentLocale = locale as Locale || this.locale; 68 | 69 | if (this.cachedOutput === null || currentLocale !== this.cachedLocale) { 70 | this.cachedLocale = currentLocale; 71 | 72 | this.cachedOutput = value !== null && Number.isFinite(+value) ? formatNumber(+value, currentLocale, format) : ''; 73 | } 74 | 75 | return this.cachedOutput; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/store/version/version.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, createReducer, on } from '@ngrx/store'; 2 | 3 | import { ServerErrorValueInterface, VersionInterface } from 'src/app/shared/interfaces'; 4 | import { VersionState, versionActions } from 'src/app/store'; 5 | import { environment } from 'src/environments/environment'; 6 | 7 | // Initial state of `Version` store. 8 | const initialState: VersionState = { 9 | frontend: environment.version, 10 | backend: '0.0.0', 11 | isLoadingFrontend: false, 12 | isLoadingBackend: false, 13 | error: null, 14 | }; 15 | 16 | const reducer = createReducer( 17 | initialState, 18 | /** 19 | * Reducer for `versionActions.fetchFrontendVersion` where we want to set 20 | * loading state of frontend version to `true`. 21 | */ 22 | on( 23 | versionActions.fetchFrontendVersion, 24 | (state: VersionState): VersionState => ({ 25 | ...state, 26 | isLoadingFrontend: true, 27 | error: null, 28 | }), 29 | ), 30 | /** 31 | * Reducer for `versionActions.fetchFrontendVersionSuccess` where we're 32 | * setting frontend version to current state. 33 | */ 34 | on( 35 | versionActions.fetchFrontendVersionSuccess, 36 | (state: VersionState, { version }: VersionInterface): VersionState => ({ 37 | ...state, 38 | isLoadingFrontend: false, 39 | frontend: version, 40 | }), 41 | ), 42 | /** 43 | * Reducer for `versionActions.fetchFrontendVersionFailure` where we're 44 | * storing occurred error to store. 45 | */ 46 | on( 47 | versionActions.fetchFrontendVersionFailure, 48 | (state: VersionState, { error }: ServerErrorValueInterface): VersionState => ({ 49 | ...state, 50 | isLoadingFrontend: false, 51 | error, 52 | }), 53 | ), 54 | /** 55 | * Reducer for `versionActions.fetchBackendVersion` where we want to set 56 | * loading state of backend version to `true`. 57 | */ 58 | on( 59 | versionActions.fetchBackendVersion, 60 | (state: VersionState): VersionState => ({ 61 | ...state, 62 | isLoadingBackend: true, 63 | error: null, 64 | }), 65 | ), 66 | /** 67 | * Reducer for `versionActions.fetchBackendVersionSuccess` where we're 68 | * storing backend version to current state. 69 | */ 70 | on( 71 | versionActions.fetchBackendVersionSuccess, 72 | (state: VersionState, { version }: VersionInterface): VersionState => ({ 73 | ...state, 74 | isLoadingBackend: false, 75 | backend: version, 76 | }), 77 | ), 78 | /** 79 | * Reducer for `versionActions.fetchBackendVersionFailure` where we're 80 | * storing occurred error to store. 81 | */ 82 | on( 83 | versionActions.fetchBackendVersionFailure, 84 | (state: VersionState, { error }: ServerErrorValueInterface): VersionState => ({ 85 | ...state, 86 | isLoadingBackend: false, 87 | error, 88 | }), 89 | ), 90 | ); 91 | 92 | // Export error `Version` store reducer. 93 | export const versionReducer = (state: VersionState|undefined, action: Action): VersionState => reducer(state, action); 94 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # manual button click from the GitHub UI 8 | workflow_dispatch: 9 | # For Branch-Protection check. Only the default branch is supported. See 10 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 11 | branch_protection_rule: 12 | # To guarantee Maintained check is occasionally updated. See 13 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 14 | schedule: 15 | - cron: '29 9 * * 5' # Run every Friday at 09:29 UTC 16 | push: 17 | branches: [ "master" ] 18 | 19 | # Declare default permissions as read only. 20 | permissions: read-all 21 | 22 | jobs: 23 | analysis: 24 | name: Scorecard analysis 25 | runs-on: ubuntu-latest 26 | permissions: 27 | # Needed to upload the results to code-scanning dashboard. 28 | security-events: write 29 | # Used to receive a badge. (Upcoming feature) 30 | id-token: write 31 | # Needs for private repositories. 32 | contents: read 33 | actions: read 34 | 35 | steps: 36 | - name: "Checkout code" 37 | uses: actions/checkout@v6.0.0 38 | with: 39 | persist-credentials: false 40 | 41 | - name: "Run analysis" 42 | uses: ossf/scorecard-action@v2.4.3 43 | with: 44 | results_file: results.sarif 45 | results_format: sarif 46 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 47 | # - you want to enable the Branch-Protection check on a *public* repository, or 48 | # - you are installing Scorecard on a *private* repository 49 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 50 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 51 | 52 | # Public repositories: 53 | # - Publish results to OpenSSF REST API for easy access by consumers 54 | # - Allows the repository to include the Scorecard badge. 55 | # - See https://github.com/ossf/scorecard-action#publishing-results. 56 | # For private repositories: 57 | # - `publish_results` will always be set to `false`, regardless 58 | # of the value entered here. 59 | publish_results: true 60 | 61 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 62 | # format to the repository Actions tab. 63 | - name: "Upload artifact" 64 | uses: actions/upload-artifact@v5.0.0 65 | with: 66 | name: SARIF file 67 | path: results.sarif 68 | retention-days: 5 69 | 70 | # Upload the results to GitHub's code scanning dashboard. 71 | - name: "Upload to code-scanning" 72 | uses: github/codeql-action/upload-sarif@v4 73 | with: 74 | sarif_file: results.sarif 75 | -------------------------------------------------------------------------------- /doc/SPEED_UP_DOCKER_COMPOSE.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This document contains information about how you can speed up docker-compos 4 | usage in development stage. 5 | 6 | ## Table of Contents 7 | 8 | * [What is this?](#what-is-this) 9 | * [Table of Contents](#table-of-contents) 10 | * [Reasons?](#reasons-table-of-contents) 11 | * [Environments with problems](#environments-with-problems-table-of-contents) 12 | * [Windows](#windows-table-of-contents) 13 | * [Mac](#mac-table-of-contents) 14 | * [Linux](#linux-table-of-contents) 15 | * [Installation of docker-sync](#installation-of-docker-sync-table-of-contents) 16 | * [Configuration](#configuration-table-of-contents) 17 | * [Startup](#startup-table-of-contents) 18 | 19 | ## Reasons? [ᐞ](#table-of-contents) 20 | 21 | IO traffic is the main reason why using Docker on certain environments 22 | will be quite slow. 23 | 24 | ## Environments with problems [ᐞ](#table-of-contents) 25 | 26 | Basically Windows and Mac; with linux you should not have these problems at all. 27 | 28 | ### Windows [ᐞ](#table-of-contents) 29 | 30 | The "most" clean solution to solve this atm is to run eg. Ubuntu desktop within 31 | [VMware](https://www.vmware.com/) / [VirtualBox](https://www.virtualbox.org/) 32 | machines. And this means that you actually run your favorite IDE inside that 33 | virtual machine. 34 | 35 | Another way is to use [docker-sync](#installation-of-docker-sync). Application 36 | itself already contains necessary [docker-sync.yml](../docker-sync.yml) 37 | configuration file to help with this. 38 | 39 | ### Mac [ᐞ](#table-of-contents) 40 | 41 | With Mac there is a bit speed difference versus pure _*inux_ installation, but 42 | you could try to speed that up by using [Docker for Mac Edge](https://docs.docker.com/docker-for-mac/edge-release-notes/) 43 | 44 | Some benchmark about `Docker for Mac` versus `Docker for Mac Edge` 45 | [here](https://medium.com/@somwhatparanoid/tweaking-docker-for-mac-performance-for-php-and-symfony-b63f3395a1da) 46 | 47 | And if that [Docker for Mac Edge](https://docs.docker.com/docker-for-mac/edge-release-notes/) 48 | isn't fast enough for you, you could also setup that [docker-sync](#installation-of-docker-sync) 49 | for your environment. 50 | 51 | ### Linux [ᐞ](#table-of-contents) 52 | 53 | No need to do anything `¯\_(ツ)_/¯` 54 | 55 | ## Installation of docker-sync [ᐞ](#table-of-contents) 56 | 57 | Follow install instructions from [docker-sync](http://docker-sync.io/) 58 | website. 59 | 60 | ### Configuration [ᐞ](#table-of-contents) 61 | 62 | Create a `compose.override.yaml` file with following content: 63 | 64 | ```yaml 65 | # 66 | # This file should NOT be added to your VCS, only purpose of this is to 67 | # override those volumes with docker-sync.yml config 68 | # 69 | services: 70 | node: 71 | volumes: 72 | - frontend-code:/app:cached 73 | volumes: 74 | frontend-code: 75 | external: true 76 | ``` 77 | 78 | ### Startup [ᐞ](#table-of-contents) 79 | 80 | To start application you just need to use command; 81 | 82 | ```bash 83 | docker-sync-stack start 84 | ``` 85 | 86 | --- 87 | 88 | [Back to resources index](README.md) - [Back to main README.md](../README.md) 89 | -------------------------------------------------------------------------------- /src/app/shared/services/configuration-service.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfigurationInterface } from 'src/app/shared/interfaces'; 2 | import { environment } from 'src/environments/environment'; 3 | 4 | export class ConfigurationService { 5 | public static configuration: ApplicationConfigurationInterface; 6 | public static initialized: boolean = false; 7 | 8 | private static configurationFile: string = `/assets/config/config.${environment.name}.json`; 9 | private static configurationFileLocal: string = `/assets/config/config.${environment.name}.local.json`; 10 | 11 | /** 12 | * Method to initialize application configuration for production and 13 | * non-production environments. This method is called only from `main.ts` 14 | * file when application is booting - so that we can be sure that proper 15 | * configuration has been loaded _before_ application itself is started. 16 | */ 17 | public static init(): Promise { 18 | return environment.production 19 | ? ConfigurationService.loadConfiguration(ConfigurationService.configurationFile) 20 | : ConfigurationService.loadDevelopment(); 21 | } 22 | 23 | /** 24 | * Method to load `development` configuration if such exists and fallback to 25 | * default configuration if there isn't development configuration. This will 26 | * also show warning message in console if that `development` configuration 27 | * is not present. 28 | */ 29 | private static loadDevelopment(): Promise { 30 | return new Promise((resolve: () => void, reject: (error: Error) => void): void => { 31 | ConfigurationService.loadConfiguration(ConfigurationService.configurationFileLocal) 32 | .then((): void => resolve()) 33 | .catch((error: string): void => { 34 | console.warn(error); 35 | console.warn(`Fallback to '${ConfigurationService.configurationFile}' configuration file`); 36 | 37 | ConfigurationService.loadConfiguration(ConfigurationService.configurationFile) 38 | .then((): void => resolve()) 39 | .catch((errorDefault: string): void => reject(new Error(errorDefault))); 40 | }); 41 | }); 42 | } 43 | 44 | /** 45 | * Method to load specified configuration file that is going to be used with 46 | * application. 47 | */ 48 | private static loadConfiguration(configurationFile: string): Promise { 49 | const ts = Math.round((new Date()).getTime() / 1000); 50 | 51 | return new Promise((resolve: () => void, reject: (error: Error) => any): void => { 52 | fetch(`${configurationFile}?t=${ts}`) 53 | .then((response: Response): void => { 54 | response 55 | .json() 56 | .then((configuration: ApplicationConfigurationInterface): void => { 57 | ConfigurationService.configuration = configuration; 58 | ConfigurationService.initialized = true; 59 | 60 | resolve(); 61 | }) 62 | .catch((error: string): void => reject(new Error(`Invalid JSON in file '${configurationFile}' - ${error}`))); 63 | }) 64 | .catch((error: string): void => reject(new Error(`Could not load file '${configurationFile}' - ${error}`))); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/shared/interceptors/backend-version.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; 2 | import { Injectable, inject } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { noop, Observable, of } from 'rxjs'; 5 | import { filter, map, tap, withLatestFrom } from 'rxjs/operators'; 6 | 7 | import { ConfigurationService } from 'src/app/shared/services'; 8 | import { versionActions, versionSelectors } from 'src/app/store'; 9 | 10 | @Injectable() 11 | export class BackendVersionInterceptor implements HttpInterceptor { 12 | private readonly store: Store = inject(Store); 13 | 14 | /** 15 | * Backend version interceptor which purpose is to update backend version 16 | * information to application footer and trigger frontend version information 17 | * fetch instantly - just to make sure that our frontend is using the latest 18 | * version. 19 | */ 20 | public intercept(httpRequest: HttpRequest, delegate: HttpHandler): Observable> { 21 | return delegate 22 | .handle(httpRequest) 23 | .pipe(tap({ 24 | next: (event: HttpEvent): void => this.handle(of(event)), 25 | error: noop, 26 | })); 27 | } 28 | 29 | /** 30 | * Steps in here; 31 | * 1) We need `HttpResponse` event 32 | * 2) Request was made against our backend 33 | * 3) Request URL doesn't contain `/version` 34 | * 4) Response headers contains `X-API-VERSION` 35 | * 5) Fetch latest backend version from store 36 | * 6) Current version from store isn't `initial` value and it differs from 37 | * response header value 38 | * 39 | * And if all of those steps are ok, then we dispatch following action; 40 | * - newBackendVersion 41 | * 42 | * This will update version information to footer component and also trigger 43 | * frontend version check instantly - just to make sure that we're using the 44 | * latest version of frontend application. 45 | * 46 | * If frontend version changes that will trigger opening a dialog that tells 47 | * user to reload application OR continue using it with old version. 48 | */ 49 | private handle(httpEvent: Observable>): void { 50 | const apiUrl: string = ConfigurationService.configuration.apiUrl; 51 | 52 | httpEvent 53 | .pipe( 54 | filter((event: any): boolean => event instanceof HttpResponse), 55 | filter((event: HttpResponse): boolean => new URL(event.url ?? '').host === new URL(apiUrl).host), 56 | filter((event: HttpResponse): boolean => !event.url?.includes('/version')), 57 | filter((event: HttpResponse): boolean => event.headers.has('X-API-VERSION')), 58 | withLatestFrom(this.store.select(versionSelectors.selectBackendVersion)), 59 | filter(([event, version]: [HttpResponse, string]): boolean => 60 | version !== '0.0.0' && event.headers.get('X-API-VERSION') !== version, 61 | ), 62 | map(([event]: [HttpResponse, string]): string => event.headers.get('X-API-VERSION') ?? ''), 63 | ) 64 | .subscribe((backendVersion: string): void => this.store.dispatch(versionActions.newBackendVersion({ backendVersion }))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/store/layout/layout.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | 3 | import { Language, Locale, Theme, Viewport } from 'src/app/shared/enums'; 4 | import { DictionaryInterface, LocalizationInterface } from 'src/app/shared/interfaces'; 5 | import { LayoutType } from 'src/app/store/store.types'; 6 | 7 | /** 8 | * Layout store actions definitions, each of these actions will change 9 | * the state of this store. 10 | * 11 | * Simple usage example; 12 | * 13 | * public constructor(private store: Store) { 14 | * this.subscription = new Subscription(); 15 | * } 16 | * 17 | * public ngOnInit(): void { 18 | * // Subscribe to language changes 19 | * this.subscriptions 20 | * .add(this.store 21 | * .select(layoutSelectors.language) 22 | * .subscribe((language: Language): void => console.log(language)), 23 | * ); 24 | * } 25 | * 26 | * public ngOnDestroy(): void { 27 | * this.subscription.unsubscribe(); 28 | * } 29 | * 30 | * public changeLanguage(language: Language): void { 31 | * // Dispatch action to change language 32 | * this.store.dispatch(layoutActions.changeLanguage({ language })); 33 | * } 34 | */ 35 | 36 | // Common actions for layout feature store 37 | const changeLanguage = createAction(LayoutType.CHANGE_LANGUAGE, props<{ language: Language }>()); 38 | const changeLocale = createAction(LayoutType.CHANGE_LOCALE, props<{ locale: Locale }>()); 39 | const changeTimezone = createAction(LayoutType.CHANGE_TIMEZONE, props<{ timezone: string }>()); 40 | const changeTheme = createAction(LayoutType.CHANGE_THEME, props<{ theme: Theme }>()); 41 | const setLanguage = createAction(LayoutType.SET_LANGUAGE, props<{ language: Language }>()); 42 | const scrollTo = createAction(LayoutType.SCROLL_TO, props<{ anchor: string, instant?: boolean }>()); 43 | 44 | /** 45 | * Action to trigger browser to scroll to top of the page. This action is 46 | * dispatched with every `RouterEvent.NavigationEnd` event. 47 | */ 48 | const scrollToTop = createAction(LayoutType.SCROLL_TO_TOP); 49 | 50 | // Action to trigger `language`, `locale` and `timezone` change in application 51 | const updateLocalization = createAction(LayoutType.UPDATE_LOCALIZATION, props<{ localization: LocalizationInterface }>()); 52 | 53 | const snackbarMessage = createAction( 54 | LayoutType.SNACKBAR_MESSAGE, 55 | props<{ message: string, duration?: number, params?: DictionaryInterface }>(), 56 | ); 57 | 58 | /** 59 | * Action to change viewport - This is to be used only in our application 60 | * main component, where we detect viewport changes. 61 | * 62 | * @internal 63 | */ 64 | const changeViewport = createAction(LayoutType.CHANGE_VIEWPORT, props<{ viewport: Viewport }>()); 65 | 66 | /** 67 | * Action to clear layout store current `scrollTo` information. 68 | * 69 | * @internal 70 | */ 71 | const clearScrollTo = createAction(LayoutType.CLEAR_SCROLL_TO); 72 | 73 | // Export all `Layout` store actions, so that those can be used easily. 74 | export const layoutActions = { 75 | changeLanguage, 76 | changeLocale, 77 | changeTimezone, 78 | changeTheme, 79 | setLanguage, 80 | scrollTo, 81 | scrollToTop, 82 | updateLocalization, 83 | snackbarMessage, 84 | clearScrollTo, 85 | changeViewport, 86 | }; 87 | -------------------------------------------------------------------------------- /src/app/shared/pipes/local-date.pipe.ts: -------------------------------------------------------------------------------- 1 | import { OnDestroy, Pipe, PipeTransform, inject } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { DateTime } from 'luxon'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | import { Locale } from 'src/app/shared/enums'; 7 | import { LocalizationInterface } from 'src/app/shared/interfaces'; 8 | import { layoutSelectors } from 'src/app/store'; 9 | 10 | /** 11 | * Locale and timezone aware date formatter pipe that can be used shorthand 12 | * version instead of using multiple `am*` pipes. 13 | * 14 | * Usage; 15 | * {{ '1982-10-12T15:59:11+00:00' | localDate : 'LLLL' }} 16 | * 17 | * See documentation about available tokens: 18 | * - https://moment.github.io/luxon/#/formatting?id=table-of-tokens 19 | * 20 | * Note that this pipe isn't `pure` one, because otherwise we cannot get those 21 | * possible locale / timezone changes to work as expected. Internally this pipe 22 | * is using local cache, so that we don't do fire unnecessary moment function 23 | * calls on every change-detection cycle. 24 | */ 25 | @Pipe({ 26 | name: 'localDate', 27 | pure: false, 28 | }) 29 | export class LocalDatePipe implements PipeTransform, OnDestroy { 30 | private locale: Locale = Locale.DEFAULT; 31 | private timezone: string = 'Europe/Helsinki'; 32 | private cachedLocale: Locale = Locale.DEFAULT; 33 | private cachedTimezone: string = 'Europe/Helsinki'; 34 | private cachedOutput: string|null = null; 35 | private readonly store: Store = inject(Store); 36 | private readonly subscriptions: Subscription = new Subscription(); 37 | 38 | /** 39 | * Constructor of the class, where we DI all services that we need to use 40 | * within this component and initialize needed properties. 41 | */ 42 | public constructor() { 43 | // Subscribe to localization changes 44 | this.subscriptions 45 | .add(this.store 46 | .select(layoutSelectors.selectLocalization) 47 | .subscribe((localization: LocalizationInterface): void => { 48 | this.locale = localization.locale; 49 | this.timezone = localization.timezone; 50 | }), 51 | ); 52 | } 53 | 54 | /** 55 | * A callback method that performs custom clean-up, invoked immediately 56 | * before a directive, pipe, or service instance is destroyed. 57 | */ 58 | public ngOnDestroy(): void { 59 | this.subscriptions.unsubscribe(); 60 | } 61 | 62 | /** 63 | * Angular invokes the `transform` method with the value of a binding as the 64 | * first argument, and any parameters as the second argument in list form. 65 | * 66 | * Note that we use local cache here, so that we don't fire those moment 67 | * library function calls on every change-detection cycle. 68 | */ 69 | public transform(value: string|Date, format?: string): string { 70 | if (this.cachedOutput === null || this.cachedLocale !== this.locale || this.cachedTimezone !== this.timezone) { 71 | this.cachedLocale = this.locale; 72 | this.cachedTimezone = this.timezone; 73 | this.cachedOutput = DateTime 74 | .fromISO(value instanceof Date ? value.toDateString() : value) 75 | .setZone(this.timezone) 76 | .setLocale(this.locale) 77 | .toFormat(format || 'F'); 78 | } 79 | 80 | return this.cachedOutput; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/store/authentication/authentication.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, createReducer, on } from '@ngrx/store'; 2 | 3 | import { UserDataValueInterface, UserProfileValueInterface } from 'src/app/auth/interfaces'; 4 | import { ServerErrorValueInterface } from 'src/app/shared/interfaces'; 5 | import { AuthenticationState, authenticationActions } from 'src/app/store'; 6 | 7 | // Initial state of `Authentication` store. 8 | const initialState: AuthenticationState = { 9 | isLoading: false, 10 | isLoggedIn: false, 11 | userData: null, 12 | profile: null, 13 | error: null, 14 | }; 15 | 16 | const reducer = createReducer( 17 | initialState, 18 | /** 19 | * Login action where we want to enable `loading` state in store and reset 20 | * possible error. 21 | */ 22 | on( 23 | authenticationActions.login, 24 | (state: AuthenticationState): AuthenticationState => ({ 25 | ...state, 26 | isLoading: true, 27 | isLoggedIn: false, 28 | error: null, 29 | }), 30 | ), 31 | /** 32 | * Login success action where we disable `loading`, tell that current user 33 | * is `loggedIn` and save logged in user data to store. 34 | */ 35 | on( 36 | authenticationActions.loginSuccess, 37 | (state: AuthenticationState, { userData }: UserDataValueInterface): AuthenticationState => ({ 38 | ...state, 39 | isLoading: false, 40 | isLoggedIn: true, 41 | userData, 42 | }), 43 | ), 44 | /** 45 | * Profile fetch action where we want to enable `loading` state in store and 46 | * reset possible error. 47 | */ 48 | on( 49 | authenticationActions.profile, 50 | (state: AuthenticationState): AuthenticationState => ({ 51 | ...state, 52 | isLoading: true, 53 | profile: null, 54 | }), 55 | ), 56 | /** 57 | * Profile fetched successfully, so we want to disable `loading` state and 58 | * save user profile data to store. 59 | */ 60 | on( 61 | authenticationActions.profileSuccess, 62 | (state: AuthenticationState, { profile }: UserProfileValueInterface): AuthenticationState => ({ 63 | ...state, 64 | isLoading: false, 65 | profile, 66 | }), 67 | ), 68 | /** 69 | * Possible error actions, here we want to reset `loading` and `loggedIn` 70 | * data and save that actual error to our store. 71 | */ 72 | on( 73 | authenticationActions.loginFailure, 74 | authenticationActions.profileFailure, 75 | (state: AuthenticationState, { error }: ServerErrorValueInterface): AuthenticationState => ({ 76 | ...state, 77 | isLoading: false, 78 | isLoggedIn: false, 79 | error, 80 | }), 81 | ), 82 | on( 83 | authenticationActions.resetError, 84 | (state: AuthenticationState): AuthenticationState => ({ 85 | ...state, 86 | error: null, 87 | }), 88 | ), 89 | /** 90 | * If/when user makes logout within this application we need to reset this 91 | * store state to initial one, so that there isn't anything user related 92 | * stuff on our store. 93 | */ 94 | on(authenticationActions.logout, (): AuthenticationState => ({ ...initialState })), 95 | ); 96 | 97 | // Export `Authentication` store reducer. 98 | export const authenticationReducer = (state: AuthenticationState|undefined, action: Action): AuthenticationState => reducer(state, action); 99 | -------------------------------------------------------------------------------- /doc/COMMANDS.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | This document contains all custom commands that you can use within this 4 | application during development stage. 5 | 6 | ## Table of Contents 7 | 8 | * [What is this?](#what-is-this) 9 | * [Table of Contents](#table-of-contents) 10 | * [Makefile](#makefile-table-of-contents) 11 | * [Yarn](#yarn-table-of-contents) 12 | 13 | ## Makefile [ᐞ](#table-of-contents) 14 | 15 | This project contains `Makefile` configuration so that you can easily run 16 | some generic commands via `make` command. Below is a list of currently 17 | supported make commands, note that you can get this same list with just 18 | running `make` command: 19 | 20 | ```bash 21 | bash # Get bash inside Node container 22 | check-translations # Check missing translations 23 | docker-kill-containers # Kill all running docker containers 24 | docker-remove-containers # Remove all docker containers 25 | docker-remove-images # Remove all docker images 26 | extract-translations # Extract translations from TypeScript and HTML 27 | # template files 28 | fish # Get fish inside Node container 29 | fix # Fix TypeScript and SCSS files 30 | fix-scss # Fix SCSS files 31 | fix-ts # Fix TypeScript files 32 | generate-ssl-cert # Generate self signed SSL certificate 33 | lint # Lint TypeScript and SCSS files 34 | lint-scss # Lint SCSS files 35 | lint-ts # Lint TypeScript files 36 | project-stats # Create simple project stats 37 | start-build # Start application in development mode and build 38 | # containers 39 | start-production # Start application locally in production mode 40 | start # Start application in development mode 41 | start-yarn-prod # Run start-prod command with yarn 42 | start-yarn # Run start command with yarn 43 | stop # Stop application containers 44 | update # Upgrade dependencies via yarn interactively 45 | ``` 46 | 47 | Example: 48 | 49 | ```bash 50 | da_wunder@wunder-VirtualBox:~/PhpstormProjects/angular-ngrx-frontend$ make bash 51 | node@84a0da4d1c84:/app$ 52 | ``` 53 | 54 | ## Yarn [ᐞ](#table-of-contents) 55 | 56 | This project contains some custom scripts that are defined in [packages.json](../package.json) 57 | file that you can easily run just by using `yarn _command_here_`. Note 58 | that these yarn commands are mean to run inside Docker container, so first 59 | use `make bash` command to get shell inside container. 60 | 61 | Below you can see a list of all those custom commands that you most likely 62 | need to use at some point of development process: 63 | 64 | ```bash 65 | extract-translations # Extract translations, see TRANSLATIONS.md 66 | lint:scss # Lint SCSS files by https://stylelint.io/ 67 | lint:ts # Lint TS files by https://palantir.github.io/tslint/ 68 | ``` 69 | 70 | Example: 71 | 72 | ```bash 73 | da_wunder@wunder-VirtualBox:~/PhpstormProjects/angular-ngrx-frontend$ make bash 74 | node@84a0da4d1c84:/app$ yarn lint:scss 75 | yarn run v1.22.4 76 | $ npx stylelint '**/*.scss' 77 | Done in 2.06s. 78 | node@84a0da4d1c84:/app$ 79 | ``` 80 | 81 | --- 82 | 83 | [Back to resources index](README.md) - [Back to main README.md](../README.md) 84 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 10 | {{ 'component.header.title' | transloco }} 11 | 12 | 13 |
16 | @if (loading$ | async) { 17 |
18 | 19 |
20 | } 21 | 22 | @if (profile) { 23 | 24 | {{ profile.username }} 25 | 26 | } 27 | 28 | 35 |
36 |
37 | 38 | 39 | 43 | fingerprint 44 | 45 | {{ 'component.header.menu.login' | transloco }} 46 | 47 | 48 | 55 | 56 | 63 | 64 | 65 | 68 | 72 | person 73 | 74 | {{ 'component.header.menu.profile' | transloco }} 75 | 76 | 77 | 84 | 85 | 92 | 93 | 94 | 95 | 103 | 104 | 105 | 108 | @for (language of languages; track language) { 109 | 115 | } 116 | 117 |
118 | --------------------------------------------------------------------------------