Compare commits
27 Commits
65647ccd1f
...
10e481669e
|
@ -8,6 +8,7 @@ import { SecurityHeadersMiddleware } from './middleware/security-middleware/secu
|
||||||
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
|
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
|
||||||
import { AuthModule } from './modules/auth-module/auth.module';
|
import { AuthModule } from './modules/auth-module/auth.module';
|
||||||
import { AccessTokenGuard } from './modules/auth-module/common/guards';
|
import { AccessTokenGuard } from './modules/auth-module/common/guards';
|
||||||
|
import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -24,7 +25,12 @@ export class AppModule {
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
consumer
|
consumer
|
||||||
// TODO: Redirect via Reverse Proxy all HTTP requests to HTTPS
|
// TODO: Redirect via Reverse Proxy all HTTP requests to HTTPS
|
||||||
.apply(CspMiddleware, SecurityHeadersMiddleware, HttpsRedirectMiddleware)
|
.apply(
|
||||||
|
CspMiddleware,
|
||||||
|
SecurityHeadersMiddleware,
|
||||||
|
HttpsRedirectMiddleware,
|
||||||
|
CorsMiddleware
|
||||||
|
)
|
||||||
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CorsMiddleware implements NestMiddleware {
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
public use(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
if (this.configService.get<string>('NODE_ENV') === 'development') {
|
||||||
|
const allowedOrigin = this.configService.get<string>('CORS_ALLOW_ORIGIN');
|
||||||
|
|
||||||
|
if (req.headers.origin === allowedOrigin) {
|
||||||
|
res.header('Access-Control-Allow-Origin', allowedOrigin);
|
||||||
|
res.header(
|
||||||
|
'Access-Control-Allow-Methods',
|
||||||
|
this.configService.get<string>('CORS_ALLOW_METHODS')
|
||||||
|
);
|
||||||
|
res.header(
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
this.configService.get<string>('CORS_ALLOW_HEADERS')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(403).json({ message: 'Forbidden' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"ignorePatterns": ["projects/**/*"],
|
"ignorePatterns": ["projects/**/*"],
|
||||||
|
"plugins": ["import", "prettier", "@stylistic/eslint-plugin-ts", "sort-class-members", "unused-imports"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts"],
|
"files": ["*.ts"],
|
||||||
|
@ -9,9 +10,78 @@
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:@angular-eslint/recommended",
|
"plugin:@angular-eslint/recommended",
|
||||||
"plugin:@angular-eslint/template/process-inline-templates",
|
"plugin:@angular-eslint/template/process-inline-templates",
|
||||||
"prettier"
|
"prettier",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@stylistic/ts/lines-between-class-members": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"enforce": [
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "method" },
|
||||||
|
{ "blankLine": "always", "prev": "method", "next": "*" },
|
||||||
|
{ "blankLine": "never", "prev": "field", "next": "field" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/ts/block-spacing": ["error"],
|
||||||
|
"@stylistic/ts/brace-style": "off",
|
||||||
|
"@stylistic/ts/key-spacing": ["error", { "afterColon": true }],
|
||||||
|
"@stylistic/ts/keyword-spacing": ["error", { "before": true }],
|
||||||
|
"@stylistic/ts/no-extra-parens": ["error", "all", {
|
||||||
|
"nestedBinaryExpressions": false,
|
||||||
|
"ternaryOperandBinaryExpressions": false
|
||||||
|
}],
|
||||||
|
"@stylistic/ts/no-extra-semi": ["error"],
|
||||||
|
"@stylistic/ts/object-curly-spacing": ["error", "always"],
|
||||||
|
"@stylistic/ts/quotes": ["error", "single"],
|
||||||
|
"@stylistic/ts/semi": ["error", "always"],
|
||||||
|
"@stylistic/ts/space-before-blocks": ["error"],
|
||||||
|
"@stylistic/ts/space-before-function-paren": ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}],
|
||||||
|
"@stylistic/ts/space-infix-ops": ["error"],
|
||||||
|
// "@stylistic/ts/max-statements-per-line": ["error", { "max": 1 }],
|
||||||
|
// "@stylistic/ts/multiline-ternary": ["error", "always"],
|
||||||
|
// "@stylistic/ts/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
|
||||||
|
// "@stylistic/ts/no-confusing-arrow": ["error"],
|
||||||
|
// "@stylistic/ts/no-floating-decimal": ["error"],
|
||||||
|
// "@stylistic/ts/no-mixed-operators": ["error"],
|
||||||
|
// "@stylistic/ts/no-mixed-spaces-and-tabs": ["error"],/
|
||||||
|
// "@stylistic/ts/no-multi-spaces": ["error"],
|
||||||
|
// "@stylistic/ts/no-multiple-empty-lines": ["error", { "max": 2 }],
|
||||||
|
// "@stylistic/ts/no-tabs": ["error", { "allowIndentationTabs": true }],
|
||||||
|
// "@stylistic/ts/no-whitespace-before-property": ["error"],
|
||||||
|
// "@stylistic/ts/nonblock-statement-body-position": ["error", "below"],
|
||||||
|
// "@stylistic/ts/object-curly-newline": ["error", "always"],
|
||||||
|
// "@stylistic/ts/object-property-newline": ["error"],
|
||||||
|
// "@stylistic/ts/one-var-declaration-per-line": ["error", "always"],
|
||||||
|
// "@stylistic/ts/operator-linebreak": ["error", "before"],
|
||||||
|
// "@stylistic/ts/padded-blocks": ["error", "never"],
|
||||||
|
// "@stylistic/ts/rest-spread-spacing": ["error", "never"],
|
||||||
|
// "@stylistic/ts/semi-spacing": ["error"],
|
||||||
|
// "@stylistic/ts/semi-style": ["error", "last"],
|
||||||
|
// "@stylistic/ts/space-in-parens": ["error", "never"],
|
||||||
|
// "@stylistic/ts/space-unary-ops": ["error"],
|
||||||
|
// "@stylistic/ts/template-curly-spacing": ["error"],
|
||||||
|
// "@stylistic/ts/template-tag-spacing": ["error"],
|
||||||
|
// "@stylistic/ts/wrap-regex": ["error"],
|
||||||
|
"no-console": ["warn", { "allow": ["warn", "error"] }],
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-var": ["error"],
|
||||||
|
"eqeqeq": ["error", "always"],
|
||||||
|
"no-eval": "error",
|
||||||
|
"prefer-const": ["error", { "destructuring": "all", "ignoreReadBeforeAssign": true }],
|
||||||
|
"prettier/prettier": ["error", { "printWidth": 80 }],
|
||||||
|
"no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
"unused-imports/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"vars": "all",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"args": "after-used",
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
"@angular-eslint/directive-selector": [
|
"@angular-eslint/directive-selector": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
@ -27,6 +97,125 @@
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"style": "kebab-case"
|
"style": "kebab-case"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-member-accessibility": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"accessibility": "explicit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowExpressions": false,
|
||||||
|
"allowTypedFunctionExpressions": true,
|
||||||
|
"allowHigherOrderFunctions": false,
|
||||||
|
"allowDirectConstAssertionInArrowFunctions": false,
|
||||||
|
"allowConciseArrowFunctionExpressionsStartingWithVoid": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/member-ordering": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"default": [
|
||||||
|
"public-static-field",
|
||||||
|
"protected-static-field",
|
||||||
|
"private-static-field",
|
||||||
|
"public-instance-field",
|
||||||
|
"protected-instance-field",
|
||||||
|
"private-instance-field",
|
||||||
|
"public-constructor",
|
||||||
|
"protected-constructor",
|
||||||
|
"private-constructor",
|
||||||
|
"public-static-method",
|
||||||
|
"protected-static-method",
|
||||||
|
"private-static-method",
|
||||||
|
"public-instance-method",
|
||||||
|
"protected-instance-method",
|
||||||
|
"private-instance-method"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// https://github.com/bryanrsmith/eslint-plugin-sort-class-members -> Read Docs and replace @typescript-eslint/member-ordering
|
||||||
|
// "sort-class-members/sort-class-members": [
|
||||||
|
// 2,
|
||||||
|
// {
|
||||||
|
// "order": [
|
||||||
|
// "[public-properties]",
|
||||||
|
// "[protected-properties]",
|
||||||
|
// "[private-properties]",
|
||||||
|
// "everything-else"
|
||||||
|
// ],
|
||||||
|
// "groups": {
|
||||||
|
// "public-properties": [{ "type": "property", "accessibility": "public" }],
|
||||||
|
// "protected-properties": [{ "type": "property", "accessibility": "protected" }],
|
||||||
|
// "private-properties": [{ "type": "property", "accessibility": "private" }]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"error",
|
||||||
|
{ "blankLine": "always", "prev": "const", "next": "*" },
|
||||||
|
{ "blankLine": "always", "prev": "let", "next": "*" },
|
||||||
|
{ "blankLine": "always", "prev": "var", "next": "*" },
|
||||||
|
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"vars": "all",
|
||||||
|
"args": "after-used",
|
||||||
|
"ignoreRestSiblings": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/typedef": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"arrayDestructuring": true,
|
||||||
|
"arrowParameter": false,
|
||||||
|
"memberVariableDeclaration": true,
|
||||||
|
"objectDestructuring": true,
|
||||||
|
"parameter": true,
|
||||||
|
"propertyDeclaration": true,
|
||||||
|
"variableDeclaration": false,
|
||||||
|
"variableDeclarationIgnoreFunction": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"parent",
|
||||||
|
"sibling",
|
||||||
|
"index"
|
||||||
|
],
|
||||||
|
"pathGroups": [
|
||||||
|
{
|
||||||
|
"pattern": "@angular/**",
|
||||||
|
"group": "external",
|
||||||
|
"position": "before"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "@app/**",
|
||||||
|
"group": "internal",
|
||||||
|
"position": "before"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "@env/**",
|
||||||
|
"group": "internal",
|
||||||
|
"position": "before"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pathGroupsExcludedImportTypes": ["builtin"],
|
||||||
|
"newlines-between": "always",
|
||||||
|
"alphabetize": {
|
||||||
|
"order": "asc",
|
||||||
|
"caseInsensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -36,7 +225,14 @@
|
||||||
"plugin:@angular-eslint/template/recommended",
|
"plugin:@angular-eslint/template/recommended",
|
||||||
"plugin:@angular-eslint/template/accessibility"
|
"plugin:@angular-eslint/template/accessibility"
|
||||||
],
|
],
|
||||||
"rules": {}
|
"rules": {
|
||||||
|
"@angular-eslint/template/elements-content": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowList": ["label"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
coverage
|
coverage
|
||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
|
@ -4,5 +4,7 @@
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"endOfLine": "auto",
|
"endOfLine": "auto",
|
||||||
"bracketSameLine": true,
|
"bracketSameLine": true,
|
||||||
"htmlWhitespaceSensitivity": "ignore"
|
"htmlWhitespaceSensitivity": "ignore",
|
||||||
|
"printWidth": 80,
|
||||||
|
"bracketSpacing": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,11 @@
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"lint": "ng lint",
|
"lint:check": "ng lint",
|
||||||
"prettier:fix": "npx prettier --write .",
|
"lint:fix": "ng lint --fix",
|
||||||
"prettier:check": "npx prettier --check .",
|
"prettier:fix": "prettier --write .",
|
||||||
|
"prettier:check": "prettier --check .",
|
||||||
|
"format": "pnpm run lint:fix && pnpm run prettier:fix",
|
||||||
"ngrok-tunnel": "pnpm wait-on http://localhost:4200 && ngrok http --domain=commonly-hot-airedale.ngrok-free.app 4200"
|
"ngrok-tunnel": "pnpm wait-on http://localhost:4200 && ngrok http --domain=commonly-hot-airedale.ngrok-free.app 4200"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -26,6 +28,9 @@
|
||||||
"@angular/platform-browser": "^17.3.0",
|
"@angular/platform-browser": "^17.3.0",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||||
"@angular/router": "^17.3.0",
|
"@angular/router": "^17.3.0",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"dompurify": "^3.1.3",
|
||||||
"primeng": "^17.11.0",
|
"primeng": "^17.11.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
@ -40,12 +45,20 @@
|
||||||
"@angular-eslint/template-parser": "17.2.1",
|
"@angular-eslint/template-parser": "17.2.1",
|
||||||
"@angular/cli": "^17.3.0",
|
"@angular/cli": "^17.3.0",
|
||||||
"@angular/compiler-cli": "^17.3.0",
|
"@angular/compiler-cli": "^17.3.0",
|
||||||
|
"@stylistic/eslint-plugin": "^2.1.0",
|
||||||
|
"@stylistic/eslint-plugin-migrate": "^2.1.0",
|
||||||
|
"@stylistic/eslint-plugin-ts": "^2.1.0",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||||
"@typescript-eslint/parser": "6.19.0",
|
"@typescript-eslint/parser": "6.19.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-sort-class-members": "^1.20.0",
|
||||||
|
"eslint-plugin-unused-imports": "^3.2.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-preset-angular": "^14.0.3",
|
"jest-preset-angular": "^14.0.3",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,2 +1 @@
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<button (click)="connectMollie()">Connect with Mollie</button>
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
|
@ -11,19 +12,23 @@ describe('AppComponent', () => {
|
||||||
it('should create the app', () => {
|
it('should create the app', () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
|
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should have the 'frontend' title`, () => {
|
it('should have the "frontend" title', () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
|
|
||||||
expect(app.title).toEqual('frontend');
|
expect(app.title).toEqual('frontend');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render title', () => {
|
it('should render title', () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
expect(compiled.querySelector('h1')?.textContent).toContain(
|
expect(compiled.querySelector('h1')?.textContent).toContain(
|
||||||
'Hello, frontend'
|
'Hello, frontend'
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
import { AuthService } from './shared/service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
providers: [],
|
providers: [AuthService],
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
constructor() {}
|
private readonly authService: AuthService = inject(AuthService);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { ApplicationConfig } from '@angular/core';
|
import { ApplicationConfig } from '@angular/core';
|
||||||
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
|
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
import { AuthInterceptor } from './shared/interceptors/auth.interceptor';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [provideRouter(routes, withComponentInputBinding()), provideAnimations()],
|
providers: [
|
||||||
|
provideHttpClient(withInterceptors([AuthInterceptor])),
|
||||||
|
provideRouter(routes, withComponentInputBinding()),
|
||||||
|
provideAnimations(),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', pathMatch: 'full', redirectTo: ''},
|
{ path: '', pathMatch: 'full', redirectTo: '' },
|
||||||
|
{
|
||||||
|
path: 'signup',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/register-root/register-root.component').then(
|
||||||
|
(m) => m.RegisterRootComponent
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
<div id="background">
|
||||||
|
<div class="img-zone">
|
||||||
|
<div class="img-wrapper">
|
||||||
|
<h1>Hi, Welcome to Ticket App.</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content-zone">
|
||||||
|
<h1>
|
||||||
|
@if (isSignupSignal()) {
|
||||||
|
Anmelden
|
||||||
|
} @else if (isRegisterSignal()) {
|
||||||
|
Registrieren
|
||||||
|
} @else {
|
||||||
|
Erste Schritte
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
@if (isDisplayButtons()) {
|
||||||
|
<div class="action">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="Anmelden"
|
||||||
|
(click)="toggleAction('signup')"></button>
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="Registrieren"
|
||||||
|
(click)="toggleAction('register')"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isSignupSignal() || isRegisterSignal()) {
|
||||||
|
<div class="register-wrapper">
|
||||||
|
@if (form) {
|
||||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
|
<div class="e-mail">
|
||||||
|
<div class="label">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
pInputText
|
||||||
|
id="email"
|
||||||
|
formControlName="email"
|
||||||
|
aria-describedby="e-mail" />
|
||||||
|
</div>
|
||||||
|
<div class="password">
|
||||||
|
<div class="label">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
</div>
|
||||||
|
<p-password
|
||||||
|
class="custom-p-password"
|
||||||
|
id="password"
|
||||||
|
formControlName="password"
|
||||||
|
aria-describedby="password"
|
||||||
|
[toggleMask]="true"></p-password>
|
||||||
|
</div>
|
||||||
|
@if (isRegisterSignal()) {
|
||||||
|
<div class="terms">
|
||||||
|
<p-checkbox
|
||||||
|
formControlName="terms"
|
||||||
|
label="Ich habe die AGB gelesen und stimme zu."
|
||||||
|
name="terms"
|
||||||
|
[binary]="true"></p-checkbox>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="signup">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="submit"
|
||||||
|
[label]="
|
||||||
|
isSignupSignal()
|
||||||
|
? 'Anmelden'
|
||||||
|
: '✨ Jetzt KOSTENFREI loslegen ✨'
|
||||||
|
"></button>
|
||||||
|
</div>
|
||||||
|
<div class="change-mask">
|
||||||
|
<a
|
||||||
|
(click)="switchMask()"
|
||||||
|
(keyup.enter)="switchMask()"
|
||||||
|
tabindex="0">
|
||||||
|
@if (isSignupSignal()) {
|
||||||
|
Kein Account? Erstellen Sie jetzt KOSTENFREI einen!
|
||||||
|
} @else {
|
||||||
|
Schon einen Account? Hier einloggen
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,94 @@
|
||||||
|
#background {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-zone {
|
||||||
|
flex: 65;
|
||||||
|
background-color: lightsalmon;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.img-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4em;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-zone {
|
||||||
|
flex: 35;
|
||||||
|
background-color: lightcyan;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 0 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.e-mail,
|
||||||
|
.password,
|
||||||
|
.terms {
|
||||||
|
.label {
|
||||||
|
font-size: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
::ng-deep p-password.custom-p-password div input {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms {
|
||||||
|
padding-top: 1.33em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password {
|
||||||
|
padding-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup {
|
||||||
|
padding-top: 3em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.change-mask {
|
||||||
|
padding-top: 1.33em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
WritableSignal,
|
||||||
|
signal,
|
||||||
|
effect,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
FormBuilder,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
|
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { CheckboxModule } from 'primeng/checkbox';
|
||||||
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
|
import { PasswordModule } from 'primeng/password';
|
||||||
|
|
||||||
|
import { AuthService } from '../../shared/service';
|
||||||
|
import { LoginCredentials } from '../../shared/types';
|
||||||
|
import {
|
||||||
|
customEmailValidator,
|
||||||
|
customPasswordValidator,
|
||||||
|
} from '../../shared/validator';
|
||||||
|
|
||||||
|
type AuthAction = 'register' | 'signup';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-register-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
InputTextModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ButtonModule,
|
||||||
|
CheckboxModule,
|
||||||
|
PasswordModule,
|
||||||
|
HttpClientModule,
|
||||||
|
],
|
||||||
|
providers: [AuthService],
|
||||||
|
templateUrl: './register-root.component.html',
|
||||||
|
styleUrl: './register-root.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class RegisterRootComponent implements OnInit {
|
||||||
|
public form: FormGroup | undefined;
|
||||||
|
public isRegisterSignal: WritableSignal<boolean> = signal(false);
|
||||||
|
public isSignupSignal: WritableSignal<boolean> = signal(false);
|
||||||
|
public isDisplayButtons: WritableSignal<boolean> = signal(true);
|
||||||
|
public emailInvalid: WritableSignal<string | null> = signal(null);
|
||||||
|
public passwordInvalid: WritableSignal<string | null> = signal(null);
|
||||||
|
public termsInvalid: WritableSignal<string | null> = signal(null);
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly formBuilder: FormBuilder,
|
||||||
|
private readonly authService: AuthService
|
||||||
|
) {
|
||||||
|
effect(() => {
|
||||||
|
if (this.form) {
|
||||||
|
if (this.isRegisterSignal()) {
|
||||||
|
this.form.addControl(
|
||||||
|
'terms',
|
||||||
|
new FormControl(false, Validators.requiredTrue)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.form.removeControl('terms');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.initializeForm();
|
||||||
|
this.setupValueChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleAction(action: AuthAction): void {
|
||||||
|
if (action === 'register') {
|
||||||
|
this.isRegisterSignal.set(true);
|
||||||
|
this.isSignupSignal.set(false);
|
||||||
|
} else {
|
||||||
|
this.isRegisterSignal.set(false);
|
||||||
|
this.isSignupSignal.set(true);
|
||||||
|
}
|
||||||
|
this.isDisplayButtons.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSubmit(): void {
|
||||||
|
this.markControlsAsTouchedAndDirty(['email', 'password', 'terms']);
|
||||||
|
|
||||||
|
if (this.form?.valid) {
|
||||||
|
if (this.isRegisterSignal()) {
|
||||||
|
this.register(this.form.value);
|
||||||
|
} else {
|
||||||
|
this.signin(this.form.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public switchMask(): void {
|
||||||
|
this.resetFormValidation();
|
||||||
|
|
||||||
|
if (this.isSignupSignal()) {
|
||||||
|
this.isSignupSignal.set(false);
|
||||||
|
this.isRegisterSignal.set(true);
|
||||||
|
} else if (this.isRegisterSignal()) {
|
||||||
|
this.isSignupSignal.set(true);
|
||||||
|
this.isRegisterSignal.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeForm(): void {
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
email: new FormControl('', {
|
||||||
|
validators: [Validators.required, customEmailValidator()],
|
||||||
|
updateOn: 'change',
|
||||||
|
}),
|
||||||
|
password: new FormControl('', {
|
||||||
|
validators: [Validators.required, customPasswordValidator()],
|
||||||
|
updateOn: 'change',
|
||||||
|
}),
|
||||||
|
terms: new FormControl(false, {
|
||||||
|
validators: [Validators.requiredTrue],
|
||||||
|
updateOn: 'change',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupValueChanges(): void {
|
||||||
|
this.setupEmailValueChanges();
|
||||||
|
this.setupPasswordValueChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEmailValueChanges(): void {
|
||||||
|
const emailControl = this.form?.get('email');
|
||||||
|
|
||||||
|
emailControl?.valueChanges.subscribe((value: string) => {
|
||||||
|
if (value?.length >= 4) {
|
||||||
|
emailControl.setValidators([
|
||||||
|
Validators.required,
|
||||||
|
customEmailValidator(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
emailControl.setValidators([
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(4),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
emailControl.updateValueAndValidity({ emitEvent: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupPasswordValueChanges(): void {
|
||||||
|
const passwordControl = this.form?.get('password');
|
||||||
|
|
||||||
|
passwordControl?.valueChanges.subscribe((value: string) => {
|
||||||
|
if (value?.length >= 8) {
|
||||||
|
passwordControl.setValidators([
|
||||||
|
Validators.required,
|
||||||
|
customPasswordValidator(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
passwordControl.setValidators([
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(8),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
passwordControl.updateValueAndValidity({ emitEvent: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private markControlsAsTouchedAndDirty(controlNames: string[]): void {
|
||||||
|
controlNames.forEach((controlName: string) => {
|
||||||
|
const control = this.form?.get(controlName);
|
||||||
|
|
||||||
|
if (control) {
|
||||||
|
control.markAsTouched();
|
||||||
|
control.markAsDirty();
|
||||||
|
control.updateValueAndValidity();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetFormValidation(): void {
|
||||||
|
['email', 'password', 'terms'].forEach((controlName: string) => {
|
||||||
|
this.resetControlValidation(controlName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetControlValidation(controlName: string): void {
|
||||||
|
const control = this.form?.get(controlName);
|
||||||
|
|
||||||
|
if (control) {
|
||||||
|
control.reset();
|
||||||
|
control.markAsPristine();
|
||||||
|
control.markAsUntouched();
|
||||||
|
control.updateValueAndValidity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private signin(logiCredentials: LoginCredentials): void {
|
||||||
|
this.authService.signin(logiCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
private register(logiCredentials: LoginCredentials): void {
|
||||||
|
this.authService.signup(logiCredentials);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
HttpErrorResponse,
|
||||||
|
HttpEvent,
|
||||||
|
HttpHandlerFn,
|
||||||
|
HttpInterceptorFn,
|
||||||
|
HttpRequest,
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable, catchError, switchMap, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
import { AuthService } from '../service';
|
||||||
|
import { Tokens } from '../types';
|
||||||
|
|
||||||
|
export const AuthInterceptor: HttpInterceptorFn = (
|
||||||
|
request: HttpRequest<unknown>,
|
||||||
|
next: HttpHandlerFn
|
||||||
|
): Observable<HttpEvent<unknown>> => {
|
||||||
|
const authService: AuthService = inject(AuthService);
|
||||||
|
const accessToken: string | null = authService.access_token;
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
request = request.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next(request).pipe(
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
return authService.refreshToken().pipe(
|
||||||
|
switchMap((tokens: Tokens) => {
|
||||||
|
request = request.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${tokens.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return next(request);
|
||||||
|
}),
|
||||||
|
catchError((refreshError) => {
|
||||||
|
authService.signout();
|
||||||
|
return throwError(() => new Error(refreshError));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => new Error());
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable, tap } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { LoginCredentials, Tokens } from '../types';
|
||||||
|
|
||||||
|
import { LocalStorageService } from './local-storage.service';
|
||||||
|
import { SessionStorageService } from './session-storage.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
private readonly _path: string = '/api/auth';
|
||||||
|
private _access_token: string | null = null;
|
||||||
|
private _refresh_token: string | null = null;
|
||||||
|
private _isAuthenticated$: BehaviorSubject<boolean> =
|
||||||
|
new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
public get access_token(): string | null {
|
||||||
|
return this._access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly httpClient: HttpClient,
|
||||||
|
private readonly localStorageService: LocalStorageService,
|
||||||
|
private readonly sessionStorageService: SessionStorageService
|
||||||
|
) {
|
||||||
|
this.autoLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public signin(credentials: LoginCredentials): void {
|
||||||
|
this.httpClient
|
||||||
|
.post<Tokens>(environment.api.base + `${this._path}/signin`, credentials)
|
||||||
|
.subscribe((response: Tokens) => {
|
||||||
|
this.handleSuccess(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public signup(credentials: LoginCredentials): void {
|
||||||
|
this.httpClient
|
||||||
|
.post<Tokens>(environment.api.base + `${this._path}/signup`, credentials)
|
||||||
|
.subscribe((response: Tokens) => {
|
||||||
|
//TODO The checked accept terms should be saved with a timestamp in the db
|
||||||
|
this.handleSuccess(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public signout(): void {
|
||||||
|
this._access_token = null;
|
||||||
|
this._refresh_token = null;
|
||||||
|
this.localStorageService.removeItem('access_token');
|
||||||
|
this.sessionStorageService.removeItem('refresh_token');
|
||||||
|
this._isAuthenticated$.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public autoLogin(): void {
|
||||||
|
const storedAccessToken: string | null =
|
||||||
|
this.localStorageService.getItem('access_token');
|
||||||
|
const storedRefreshToken: string | null =
|
||||||
|
this.sessionStorageService.getItem('refresh_token');
|
||||||
|
|
||||||
|
if (storedAccessToken && storedRefreshToken) {
|
||||||
|
this._refresh_token = storedRefreshToken;
|
||||||
|
this._isAuthenticated$.next(true);
|
||||||
|
//TODO Validate tokens with backend or decode JWT to check expiration
|
||||||
|
} else {
|
||||||
|
this.signout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public refreshToken(): Observable<Tokens> {
|
||||||
|
const headers = new HttpHeaders().set(
|
||||||
|
'Authorization',
|
||||||
|
'Bearer ' + this._refresh_token
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.httpClient
|
||||||
|
.post<Tokens>(
|
||||||
|
environment.api.base + `${this._path}/refresh`,
|
||||||
|
{},
|
||||||
|
{ headers: headers }
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
tap((response: Tokens) => {
|
||||||
|
this.handleSuccess(response);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSuccess(tokens: Tokens): void {
|
||||||
|
this._access_token = tokens.access_token;
|
||||||
|
this._refresh_token = tokens.refresh_token;
|
||||||
|
this.localStorageService.setItem('access_token', tokens.access_token);
|
||||||
|
this.sessionStorageService.setItem('refresh_token', tokens.refresh_token);
|
||||||
|
this._isAuthenticated$.next(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { EncryptionService } from './encryption.service';
|
||||||
|
import { SanitizationService } from './sanitization.service';
|
||||||
|
|
||||||
|
export abstract class BaseStorageService {
|
||||||
|
protected abstract getStorage(): Storage;
|
||||||
|
|
||||||
|
public setItem<T>(key: string, value: T): void {
|
||||||
|
const sanitizedValue = SanitizationService.sanitize(JSON.stringify(value));
|
||||||
|
const encryptedValue = EncryptionService.encrypt(sanitizedValue);
|
||||||
|
|
||||||
|
this.getStorage().setItem(key, encryptedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItem<T>(key: string): T | null {
|
||||||
|
const encryptedValue = this.getStorage().getItem(key);
|
||||||
|
|
||||||
|
if (encryptedValue) {
|
||||||
|
const decryptedValue = EncryptionService.decrypt(encryptedValue);
|
||||||
|
const sanitizedValue = SanitizationService.sanitize(decryptedValue);
|
||||||
|
|
||||||
|
return JSON.parse(sanitizedValue) as T;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(key: string): void {
|
||||||
|
this.getStorage().removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this.getStorage().clear();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
export class EncryptionService {
|
||||||
|
private static readonly key: string = environment.security.encryptionKey;
|
||||||
|
|
||||||
|
public static encrypt(data: string): string {
|
||||||
|
return CryptoJS.AES.encrypt(data, this.key).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decrypt(data: string): string {
|
||||||
|
return CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './auth.service';
|
||||||
|
export * from './local-storage.service';
|
||||||
|
export * from './session-storage.service';
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BaseStorageService } from './base-storage.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class LocalStorageService extends BaseStorageService {
|
||||||
|
protected getStorage(): Storage {
|
||||||
|
return localStorage;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
|
export class SanitizationService {
|
||||||
|
public static sanitize(data: string): string {
|
||||||
|
return DOMPurify.sanitize(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BaseStorageService } from './base-storage.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class SessionStorageService extends BaseStorageService {
|
||||||
|
protected getStorage(): Storage {
|
||||||
|
return sessionStorage;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './login-credentials';
|
||||||
|
export * from './tokens';
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type LoginCredentials = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type Tokens = {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||||
|
|
||||||
|
export function customEmailValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return { emailInvalid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 4) {
|
||||||
|
return { emailTooShort: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
|
return emailPattern.test(value) ? null : { emailInvalid: true };
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './email-validator';
|
||||||
|
export * from './password-validator';
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||||
|
|
||||||
|
export function customPasswordValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return { passwordTooShort: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.length >= 8 ? null : { passwordTooShort: true };
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,6 +2,6 @@ export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
oauth: {
|
oauth: {
|
||||||
clinetId: 'app_FLXnxSBnnaKkXoYCgk3J62iA',
|
clinetId: 'app_FLXnxSBnnaKkXoYCgk3J62iA',
|
||||||
redirectUri: 'https://commonly-hot-airedale.ngrok-free.app/oauth'
|
redirectUri: 'https://commonly-hot-airedale.ngrok-free.app/oauth',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
|
api: {
|
||||||
|
base: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
encryptionKey: 'my-secret',
|
||||||
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
clinetId: 'app_FLXnxSBnnaKkXoYCgk3J62iA',
|
clinetId: 'app_FLXnxSBnnaKkXoYCgk3J62iA',
|
||||||
redirectUri: 'https://commonly-hot-airedale.ngrok-free.app/oauth'
|
redirectUri: 'https://commonly-hot-airedale.ngrok-free.app/oauth',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { appConfig } from './app/app.config';
|
|
||||||
import { AppComponent } from './app/app.component';
|
import { AppComponent } from './app/app.component';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
// Import PrimeNG styles
|
// Import PrimeNG styles
|
||||||
@import 'primeng/resources/themes/lara-light-blue/theme.css';
|
@import 'primeng/resources/themes/lara-light-blue/theme.css';
|
||||||
@import 'primeng/resources/primeng.css';
|
@import 'primeng/resources/primeng.css';
|
||||||
|
|
||||||
|
// PrimeNG icons
|
||||||
|
@import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue