Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Admin Interface - Menu #7

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 24 additions & 61 deletions .idea/inspectionProfiles/Project_Default.xml

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Menu
# Admin Interface - Menu

- [ ] Two-layered navigation menu
- [ ] Add new site/category
- [ ] Move site/category
2 changes: 1 addition & 1 deletion src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';


const routes: Routes = [];
const routes: Routes = [{ path: 'admin', loadChildren: () => import('./features/admin/admin.module').then(m => m.AdminModule) }];

@NgModule({
imports: [RouterModule.forRoot(routes)],
Expand Down
4 changes: 2 additions & 2 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ describe('AppComponent', () => {
expect(app).toBeTruthy();
});

it(`should have as title 'Pfarre-Machstrasse'`, () => {
it(`should have as title 'Pfarre Machstrasse - Hl. Klaus von Flüe'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('Pfarre-Machstrasse');
expect(app.title).toEqual('Pfarre Machstrasse - Hl. Klaus von Flüe');
});
});
8 changes: 5 additions & 3 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Component } from '@angular/core';
import {Component, HostBinding} from '@angular/core';

@Component({
selector: 'app-root',
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'body',
templateUrl: './app.component.html'
})
export class AppComponent {
title = 'Pfarre-Machstrasse';
@HostBinding('class') class = 'mat-typography';
title = 'Pfarre Machstrasse - Hl. Klaus von Flüe';
}
4 changes: 2 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {APP_INITIALIZER, NgModule} from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import {CoreModule} from './core/core.module';
import {AuthModule} from './auth/auth.module';
import {AppConfig} from './core/config/app-config';
Expand All @@ -25,7 +25,7 @@ export const initializeApp = (appConfig: AppConfig) => (): Promise<void> => appC
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
NoopAnimationsModule,
CoreModule,
AuthModule,
MatIconModule,
Expand Down
4 changes: 2 additions & 2 deletions src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {MatDialogModule} from '@angular/material/dialog';
import {MatInputModule} from '@angular/material/input';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {HttpClientModule} from '@angular/common/http';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';


@NgModule({
Expand All @@ -26,7 +26,7 @@ import {HttpClientModule} from '@angular/common/http';
providers: [
AuthGuard,
AuthService,
AuthInterceptor
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
],
exports: [LoginFormComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
Expand Down
34 changes: 17 additions & 17 deletions src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class AuthService implements OnDestroy {
}

private get userUrl(): string {
return `${AppConfig.INSTANCE.apiEndpoint}/user`;
return `${AppConfig.INSTANCE.apiEndpoint}/self`;
}

constructor(private _router: Router, private _http: HttpClient, loggerService: LoggerService) {
Expand All @@ -48,10 +48,10 @@ export class AuthService implements OnDestroy {
}

public login(username: string, password: string): Observable<boolean> {
if (!environment.production) {
this._user$.next({username, role: 'admin'});
/*if (!environment.production) {
this._user$.next({id: '', email: username, firstName: 'Demo', lastName: 'User', role: 'admin'});
return of(true);
}
}*/
return this._http
.post<LoginResult>(this.loginUrl, { username, password })
.pipe(
Expand Down Expand Up @@ -83,8 +83,8 @@ export class AuthService implements OnDestroy {
}

public setLocalStorage(x: LoginResult): void {
localStorage.setItem(ACCESS_TOKEN, x.access_token);
localStorage.setItem(REFRESH_TOKEN, x.refresh_token ?? null);
localStorage.setItem(ACCESS_TOKEN, x.accessToken);
localStorage.setItem(REFRESH_TOKEN, x.refreshToken ?? null);
localStorage.setItem('login-event', 'login' + Math.random());
}

Expand All @@ -94,17 +94,7 @@ export class AuthService implements OnDestroy {
localStorage.setItem('logout-event', 'logout' + Math.random());
}

private getTokenRemainingTime(): number {
const accessToken = this.getAccessToken();
if (!accessToken) {
return 0;
}
const jwtToken = JSON.parse(atob(accessToken.split('.')[1]));
const expires = new Date(jwtToken.exp * 1000);
return expires.getTime() - Date.now();
}

private refreshToken(): Observable<LoginResult> {
public refreshToken(): Observable<LoginResult> {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
this.doLogout();
Expand All @@ -123,6 +113,16 @@ export class AuthService implements OnDestroy {
);
}

private getTokenRemainingTime(): number {
const accessToken = this.getAccessToken();
if (!accessToken) {
return 0;
}
const jwtToken = JSON.parse(atob(accessToken.split('.')[1]));
const expires = new Date(jwtToken.exp * 1000);
return expires.getTime() - Date.now();
}

private updateUser(): void {
this._http.get<User>(this.userUrl).subscribe(user => this._user$.next(user));
}
Expand Down
6 changes: 5 additions & 1 deletion src/app/core/config/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ export interface IConfig {
apiEndpoint: string;
}

const NULL_CONFIG: IConfig = {
apiEndpoint: null
};

@Injectable()
export class AppConfig {
private static _config: IConfig;
public static get INSTANCE(): IConfig {
return AppConfig._config;
return AppConfig._config ?? NULL_CONFIG;
}
constructor(private _http: HttpClient) {}
load(): Promise<void> {
Expand Down
5 changes: 4 additions & 1 deletion src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common';
import { LoggingModule } from './logging/logging.module';
import {ErrorInterceptor} from './error.interceptor';
import {AppConfig} from './config/app-config';
import {JsonInterceptor} from './json.interceptor';
import { HTTP_INTERCEPTORS } from '@angular/common/http';


@NgModule({
Expand All @@ -16,7 +18,8 @@ import {AppConfig} from './config/app-config';
],
providers: [
AppConfig,
ErrorInterceptor
{ provide: HTTP_INTERCEPTORS, useClass: JsonInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
]
})
export class CoreModule {
Expand Down
4 changes: 4 additions & 0 deletions src/app/core/error.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export class ErrorInterceptor implements HttpInterceptor {
this._router.navigate(['']);
this._logger.error('Authentication error {0}', err?.message ?? err?.error?.message ?? err?.statusText);
}
if (err.status === 404) {
this._router.navigate(['']);
this._logger.error('Not found error {0}', err?.message ?? err?.error?.message ?? err?.statusText);
}
const error = err?.message ?? err?.error?.message ?? err?.statusText;
return throwError(error);
})
Expand Down
16 changes: 16 additions & 0 deletions src/app/core/json.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { JsonInterceptor } from './json.interceptor';

describe('JsonInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
JsonInterceptor
]
}));

it('should be created', () => {
const interceptor: JsonInterceptor = TestBed.inject(JsonInterceptor);
expect(interceptor).toBeTruthy();
});
});
62 changes: 62 additions & 0 deletions src/app/core/json.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor, HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs';
import {map} from 'rxjs/operators';

const isArray = Array.isArray;

const isObject = (o) => o === Object(o) && !isArray(o) && typeof o !== 'function';

const toCamel = (s) => s.replace(/([-_][a-z])/ig, ($1) => $1.toUpperCase()
.replace('-', '')
.replace('_', ''));

const keysToCamel = (o) => {
if (isObject(o)) {
const n = {};

Object.keys(o)
.forEach((k) => {
const preserve = k.startsWith('_');
let key: string = toCamel(k);
if (preserve) {
key = '_'+key.charAt(0).toLowerCase()+key.slice(1);
}
n[key] = keysToCamel(o[k]);
});

return n;
} else if (isArray(o)) {
return o.map((i) => keysToCamel(i));
}

return o;
};

/**
* Intercept json responses and convert them to camelCase
*/
@Injectable()
export class JsonInterceptor implements HttpInterceptor {

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (request.responseType === 'json') {
return next.handle(request).pipe(map(value => {
if (value instanceof HttpResponse) {
try {
return value.clone({body: keysToCamel(value.body)});
} catch (ignored) {
}
}
return value;
}));
} else {
return next.handle(request);
}
}
}
26 changes: 23 additions & 3 deletions src/app/data/category.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import {Observable} from 'rxjs';
import {Page} from './page';

/**
* The DTO (DataTransferObject) for a category
*/
export interface CategoryDTO {
/**
* The id of this category
*/
id: string;
/**
* The title of this category
*/
title: string;
/**
* The ordering number of this category
*/
order: number;
}

export interface Category {
id: string;
title: string;
/**
* The full category object
*/
export interface Category extends CategoryDTO {
/**
* The direct accessor for all pages of this category<br>
* Executes a http request to fetch the pages
*
* @see Page
*/
pages$?: Observable<Page[]>;
}
32 changes: 32 additions & 0 deletions src/app/data/change.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export type ChangeType = 1 | 0 | -1;
export type ChangeData = ([ChangeType, string])[];

export interface Change {
/**
* The UUID of the User creating this change
*
* @type string uuid
*/
author: string;
/**
* The id of the category which the page of this change belongs to
*
* @see Category.id
* @see Page.category
*/
category: string;
/**
* The id of the page this change was made on
*
* @see Page.id
*/
page: string;
/**
* The timestamp of creation of this change
*/
createdAt: Date;
/**
* The data of this change
*/
data: ChangeData;
}
16 changes: 16 additions & 0 deletions src/app/data/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Observable} from 'rxjs';
import {Media} from './media';

export interface EventDTO {
id: string;
name: string;
details: string;
start: Date;
end: Date;
media: string | null;
owner: string;
}

export interface Event extends EventDTO {
media$: Observable<Media>;
}
8 changes: 8 additions & 0 deletions src/app/data/gallery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Media} from './media';

export interface Gallery {
id: string;
title: string;
owner: string;
media: Media[];
}
14 changes: 11 additions & 3 deletions src/app/data/login-result.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
/* eslint-disable @typescript-eslint/naming-convention */
/**
* The login result containing the JWT tokens for access and refresh
*/
export interface LoginResult {
access_token: string;
refresh_token?: string;
/**
* The JWT token used to authenticate with the backend
*/
accessToken: string;
/**
* The JWT token used to refresh the access token
*/
refreshToken?: string;
}
Loading