diff --git a/api/src/api_graphql/graphql/group.gql b/api/src/api_graphql/graphql/group.gql
index 503b366..82005fe 100644
--- a/api/src/api_graphql/graphql/group.gql
+++ b/api/src/api_graphql/graphql/group.gql
@@ -7,7 +7,6 @@ type GroupResult {
type Group implements DbModel {
id: ID
name: String
- description: String
deleted: Boolean
editor: User
@@ -18,7 +17,6 @@ type Group implements DbModel {
input GroupSort {
id: SortOrder
name: SortOrder
- description: SortOrder
deleted: SortOrder
editorId: SortOrder
@@ -29,7 +27,6 @@ input GroupSort {
input GroupFilter {
id: IntFilter
name: StringFilter
- description: StringFilter
deleted: BooleanFilter
editor: IntFilter
@@ -37,8 +34,18 @@ input GroupFilter {
updatedUtc: DateFilter
}
-input GroupInput {
- id: ID
- name: String
- description: String
+type GroupMutation {
+ create(input: GroupCreateInput!): Group
+ update(input: GroupUpdateInput!): Group
+ delete(id: ID!): Boolean
+ restore(id: ID!): Boolean
}
+
+input GroupCreateInput {
+ name: String!
+}
+
+input GroupUpdateInput {
+ id: ID!
+ name: String
+}
\ No newline at end of file
diff --git a/api/src/api_graphql/graphql/mutation.gql b/api/src/api_graphql/graphql/mutation.gql
index 3886ea6..3b93c89 100644
--- a/api/src/api_graphql/graphql/mutation.gql
+++ b/api/src/api_graphql/graphql/mutation.gql
@@ -3,4 +3,7 @@ type Mutation {
user: UserMutation
role: RoleMutation
+
+ group: GroupMutation
+ shortUrl: ShortUrlMutation
}
\ No newline at end of file
diff --git a/api/src/api_graphql/graphql/short_url.gql b/api/src/api_graphql/graphql/short_url.gql
index adfdb1b..48a5da3 100644
--- a/api/src/api_graphql/graphql/short_url.gql
+++ b/api/src/api_graphql/graphql/short_url.gql
@@ -41,8 +41,22 @@ input ShortUrlFilter {
updatedUtc: DateFilter
}
-input ShortUrlInput {
- id: ID
+type ShortUrlMutation {
+ create(input: ShortUrlCreateInput!): ShortUrl
+ update(input: ShortUrlUpdateInput!): ShortUrl
+ delete(id: ID!): Boolean
+ restore(id: ID!): Boolean
+}
+
+input ShortUrlCreateInput {
+ shortUrl: String!
+ targetUrl: String!
+ description: String
+ group: ID
+}
+
+input ShortUrlUpdateInput {
+ id: ID!
shortUrl: String
targetUrl: String
description: String
diff --git a/api/src/api_graphql/input/group_create_input.py b/api/src/api_graphql/input/group_create_input.py
new file mode 100644
index 0000000..482bc64
--- /dev/null
+++ b/api/src/api_graphql/input/group_create_input.py
@@ -0,0 +1,13 @@
+from api_graphql.abc.input_abc import InputABC
+
+
+class GroupCreateInput(InputABC):
+
+ def __init__(self, src: dict):
+ InputABC.__init__(self, src)
+
+ self._name = self.option("name", str, required=True)
+
+ @property
+ def name(self) -> str:
+ return self._name
diff --git a/api/src/api_graphql/input/group_update_input.py b/api/src/api_graphql/input/group_update_input.py
new file mode 100644
index 0000000..a40d665
--- /dev/null
+++ b/api/src/api_graphql/input/group_update_input.py
@@ -0,0 +1,18 @@
+from api_graphql.abc.input_abc import InputABC
+
+
+class GroupUpdateInput(InputABC):
+
+ def __init__(self, src: dict):
+ InputABC.__init__(self, src)
+
+ self._id = self.option("id", int, required=True)
+ self._name = self.option("name", str)
+
+ @property
+ def id(self) -> int:
+ return self._id
+
+ @property
+ def name(self) -> str:
+ return self._name
diff --git a/api/src/api_graphql/input/short_url_create_input.py b/api/src/api_graphql/input/short_url_create_input.py
new file mode 100644
index 0000000..c60c38c
--- /dev/null
+++ b/api/src/api_graphql/input/short_url_create_input.py
@@ -0,0 +1,30 @@
+from typing import Optional
+
+from api_graphql.abc.input_abc import InputABC
+
+
+class ShortUrlCreateInput(InputABC):
+
+ def __init__(self, src: dict):
+ InputABC.__init__(self, src)
+
+ self._short_url = self.option("shortUrl", str, required=True)
+ self._target_url = self.option("targetUrl", str, required=True)
+ self._description = self.option("description", str)
+ self._group_id = self.option("groupId", int)
+
+ @property
+ def short_url(self) -> str:
+ return self._short_url
+
+ @property
+ def target_url(self) -> str:
+ return self._target_url
+
+ @property
+ def description(self) -> Optional[str]:
+ return self._description
+
+ @property
+ def group_id(self) -> Optional[int]:
+ return self._group_id
diff --git a/api/src/api_graphql/input/short_url_update_input.py b/api/src/api_graphql/input/short_url_update_input.py
new file mode 100644
index 0000000..200cb2d
--- /dev/null
+++ b/api/src/api_graphql/input/short_url_update_input.py
@@ -0,0 +1,35 @@
+from typing import Optional
+
+from api_graphql.abc.input_abc import InputABC
+
+
+class ShortUrlUpdateInput(InputABC):
+
+ def __init__(self, src: dict):
+ InputABC.__init__(self, src)
+
+ self._id = self.option("id", int, required=True)
+ self._short_url = self.option("shortUrl", str)
+ self._target_url = self.option("targetUrl", str)
+ self._description = self.option("description", str)
+ self._group_id = self.option("groupId", int)
+
+ @property
+ def id(self) -> int:
+ return self._id
+
+ @property
+ def short_url(self) -> Optional[str]:
+ return self._short_url
+
+ @property
+ def target_url(self) -> Optional[str]:
+ return self._target_url
+
+ @property
+ def description(self) -> Optional[str]:
+ return self._description
+
+ @property
+ def group_id(self) -> Optional[int]:
+ return self._group_id
diff --git a/api/src/api_graphql/mutation.py b/api/src/api_graphql/mutation.py
index e4779cc..77b0f61 100644
--- a/api/src/api_graphql/mutation.py
+++ b/api/src/api_graphql/mutation.py
@@ -32,3 +32,13 @@ class Mutation(MutationABC):
Permissions.users_delete,
],
)
+
+ self.add_mutation_type(
+ "group",
+ "Group",
+ require_any_permission=[
+ Permissions.groups_create,
+ Permissions.groups_update,
+ Permissions.groups_delete,
+ ],
+ )
diff --git a/api/src/api_graphql/mutations/group_mutation.py b/api/src/api_graphql/mutations/group_mutation.py
new file mode 100644
index 0000000..c526366
--- /dev/null
+++ b/api/src/api_graphql/mutations/group_mutation.py
@@ -0,0 +1,73 @@
+from api_graphql.abc.mutation_abc import MutationABC
+from api_graphql.input.group_create_input import GroupCreateInput
+from api_graphql.input.group_update_input import GroupUpdateInput
+from core.logger import APILogger
+from data.schemas.public.group import Group
+from data.schemas.public.group_dao import groupDao
+from service.permission.permissions_enum import Permissions
+
+logger = APILogger(__name__)
+
+
+class GroupMutation(MutationABC):
+ def __init__(self):
+ MutationABC.__init__(self, "Group")
+
+ self.mutation(
+ "create",
+ self.resolve_create,
+ GroupCreateInput,
+ require_any_permission=[Permissions.groups_create],
+ )
+ self.mutation(
+ "update",
+ self.resolve_update,
+ GroupUpdateInput,
+ require_any_permission=[Permissions.groups_update],
+ )
+ self.mutation(
+ "delete",
+ self.resolve_delete,
+ require_any_permission=[Permissions.groups_delete],
+ )
+ self.mutation(
+ "restore",
+ self.resolve_restore,
+ require_any_permission=[Permissions.groups_delete],
+ )
+
+ @staticmethod
+ async def resolve_create(obj: GroupCreateInput, *_):
+ logger.debug(f"create group: {obj.__dict__}")
+
+ group = Group(
+ 0,
+ obj.name,
+ )
+ nid = await groupDao.create(group)
+ return await groupDao.get_by_id(nid)
+
+ @staticmethod
+ async def resolve_update(obj: GroupUpdateInput, *_):
+ logger.debug(f"update group: {input}")
+
+ if obj.name is not None:
+ group = await groupDao.get_by_id(obj.id)
+ group.name = obj.name
+ await groupDao.update(group)
+
+ return await groupDao.get_by_id(obj.id)
+
+ @staticmethod
+ async def resolve_delete(*_, id: str):
+ logger.debug(f"delete group: {id}")
+ group = await groupDao.get_by_id(id)
+ await groupDao.delete(group)
+ return True
+
+ @staticmethod
+ async def resolve_restore(*_, id: str):
+ logger.debug(f"restore group: {id}")
+ group = await groupDao.get_by_id(id)
+ await groupDao.restore(group)
+ return True
diff --git a/api/src/api_graphql/mutations/short_url_mutation.py b/api/src/api_graphql/mutations/short_url_mutation.py
new file mode 100644
index 0000000..2977683
--- /dev/null
+++ b/api/src/api_graphql/mutations/short_url_mutation.py
@@ -0,0 +1,92 @@
+from werkzeug.exceptions import NotFound
+
+from api_graphql.abc.mutation_abc import MutationABC
+from api_graphql.input.short_url_create_input import ShortUrlCreateInput
+from api_graphql.input.short_url_update_input import ShortUrlUpdateInput
+from core.logger import APILogger
+from data.schemas.public.group_dao import groupDao
+from data.schemas.public.short_url import ShortUrl
+from data.schemas.public.short_url_dao import shortUrlDao
+from service.permission.permissions_enum import Permissions
+
+logger = APILogger(__name__)
+
+
+class ShortUrlMutation(MutationABC):
+ def __init__(self):
+ MutationABC.__init__(self, "ShortUrl")
+
+ self.mutation(
+ "create",
+ self.resolve_create,
+ ShortUrlCreateInput,
+ require_any_permission=[Permissions.short_urls_create],
+ )
+ self.mutation(
+ "update",
+ self.resolve_update,
+ ShortUrlUpdateInput,
+ require_any_permission=[Permissions.short_urls_update],
+ )
+ self.mutation(
+ "delete",
+ self.resolve_delete,
+ require_any_permission=[Permissions.short_urls_delete],
+ )
+ self.mutation(
+ "restore",
+ self.resolve_restore,
+ require_any_permission=[Permissions.short_urls_delete],
+ )
+
+ @staticmethod
+ async def resolve_create(obj: ShortUrlCreateInput, *_):
+ logger.debug(f"create short_url: {obj.__dict__}")
+
+ short_url = ShortUrl(
+ 0,
+ obj.short_url,
+ obj.target_url,
+ obj.description,
+ obj.group_id,
+ )
+ nid = await shortUrlDao.create(short_url)
+ return await shortUrlDao.get_by_id(nid)
+
+ @staticmethod
+ async def resolve_update(obj: ShortUrlUpdateInput, *_):
+ logger.debug(f"update short_url: {input}")
+
+ short_url = await shortUrlDao.get_by_id(obj.id)
+
+ if obj.short_url is not None:
+ short_url.short_url = obj.short_url
+
+ if obj.target_url is not None:
+ short_url.target_url = obj.target_url
+
+ if obj.description is not None:
+ short_url.description = obj.description
+
+ if obj.group_id is not None:
+ group_by_id = await groupDao.find_by_id(obj.group_id)
+ if group_by_id is None:
+ raise NotFound(f"Group with id {obj.group_id} does not exist")
+ short_url.group_id = obj.group_id
+
+ await shortUrlDao.update(short_url)
+ return await shortUrlDao.get_by_id(obj.id)
+
+ @staticmethod
+ async def resolve_delete(*_, id: str):
+ logger.debug(f"delete short_url: {id}")
+ short_url = await shortUrlDao.get_by_id(id)
+ await shortUrlDao.delete(short_url)
+ return True
+
+ @staticmethod
+ async def resolve_restore(*_, id: str):
+ logger.debug(f"restore short_url: {id}")
+ short_url = await shortUrlDao.get_by_id(id)
+ await shortUrlDao.restore(short_url)
+ return True
diff --git a/api/src/api_graphql/queries/group_query.py b/api/src/api_graphql/queries/group_query.py
index d055c02..ee70246 100644
--- a/api/src/api_graphql/queries/group_query.py
+++ b/api/src/api_graphql/queries/group_query.py
@@ -6,4 +6,3 @@ class GroupQuery(DbModelQueryABC):
DbModelQueryABC.__init__(self, "Group")
self.set_field("name", lambda x, *_: x.name)
- self.set_field("description", lambda x, *_: x.description)
diff --git a/web/src/app/components/header/header.component.html b/web/src/app/components/header/header.component.html
index 90a7129..b298046 100644
--- a/web/src/app/components/header/header.component.html
+++ b/web/src/app/components/header/header.component.html
@@ -10,10 +10,6 @@
-
-
();
+ unsubscribe$ = new Subject();
- constructor(private sidebar: SidebarService) {
- this.sidebar.elements$
- .pipe(takeUntil(this.unsubscribe$))
- .subscribe(elements => {
- this.elements = elements;
- });
- }
+ constructor(private sidebar: SidebarService) {
+ this.sidebar.elements$
+ .pipe(takeUntil(this.unsubscribe$))
+ .subscribe(elements => {
+ this.elements = elements;
+ });
+ }
+
+ ngOnDestroy() {
+ this.unsubscribe$.next();
+ this.unsubscribe$.complete();
+ }
}
diff --git a/web/src/app/model/auth/permissionsEnum.ts b/web/src/app/model/auth/permissionsEnum.ts
index 2958a9f..184b4f2 100644
--- a/web/src/app/model/auth/permissionsEnum.ts
+++ b/web/src/app/model/auth/permissionsEnum.ts
@@ -1,31 +1,30 @@
export enum PermissionsEnum {
// Administration
- apiKeys = 'api_keys',
- apiKeysCreate = 'api_keys.create',
- apiKeysUpdate = 'api_keys.update',
- apiKeysDelete = 'api_keys.delete',
+ apiKeys = "api_keys",
+ apiKeysCreate = "api_keys.create",
+ apiKeysUpdate = "api_keys.update",
+ apiKeysDelete = "api_keys.delete",
// Users
- users = 'users',
- usersCreate = 'users.create',
- usersUpdate = 'users.update',
- usersDelete = 'users.delete',
+ users = "users",
+ usersCreate = "users.create",
+ usersUpdate = "users.update",
+ usersDelete = "users.delete",
// Permissions
- roles = 'roles',
- rolesCreate = 'roles.create',
- rolesUpdate = 'roles.update',
- rolesDelete = 'roles.delete',
+ roles = "roles",
+ rolesCreate = "roles.create",
+ rolesUpdate = "roles.update",
+ rolesDelete = "roles.delete",
// Public
- news = 'news',
- newsCreate = 'news.create',
- newsUpdate = 'news.update',
- newsDelete = 'news.delete',
+ groups = "groups",
+ groupsCreate = "groups.create",
+ groupsUpdate = "groups.update",
+ groupsDelete = "groups.delete",
- // Utils
- ipList = 'ip_list',
- ipListCreate = 'ip_list.create',
- ipListUpdate = 'ip_list.update',
- ipListDelete = 'ip_list.delete',
+ shortUrls = "short_urls",
+ shortUrlsCreate = "short_urls.create",
+ shortUrlsUpdate = "short_urls.update",
+ shortUrlsDelete = "short_urls.delete",
}
diff --git a/web/src/app/model/entities/group.ts b/web/src/app/model/entities/group.ts
new file mode 100644
index 0000000..23c945c
--- /dev/null
+++ b/web/src/app/model/entities/group.ts
@@ -0,0 +1,15 @@
+import { DbModel } from "src/app/model/entities/db-model";
+import { Permission } from "src/app/model/entities/role";
+
+export interface Group extends DbModel {
+ name?: string;
+}
+
+export interface GroupCreateInput {
+ name: string;
+}
+
+export interface GroupUpdateInput {
+ id: number;
+ name: string;
+}
diff --git a/web/src/app/modules/admin/admin.module.ts b/web/src/app/modules/admin/admin.module.ts
index 47350a5..43e0f17 100644
--- a/web/src/app/modules/admin/admin.module.ts
+++ b/web/src/app/modules/admin/admin.module.ts
@@ -1,21 +1,21 @@
-import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { RouterModule, Routes } from '@angular/router';
-import { SharedModule } from 'src/app/modules/shared/shared.module';
+import { NgModule } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { RouterModule, Routes } from "@angular/router";
+import { SharedModule } from "src/app/modules/shared/shared.module";
const routes: Routes = [
{
- path: 'public',
+ path: "groups",
loadChildren: () =>
- import('src/app/modules/admin/public/public.module').then(
- m => m.PublicModule
+ import("src/app/modules/admin/groups/groups.module").then(
+ (m) => m.GroupsModule,
),
},
{
- path: 'administration',
+ path: "administration",
loadChildren: () =>
- import('src/app/modules/admin/administration/administration.module').then(
- m => m.AdministrationModule
+ import("src/app/modules/admin/administration/administration.module").then(
+ (m) => m.AdministrationModule,
),
},
];
diff --git a/web/src/app/modules/admin/groups/form-page/group-form-page.component.html b/web/src/app/modules/admin/groups/form-page/group-form-page.component.html
new file mode 100644
index 0000000..82d8e33
--- /dev/null
+++ b/web/src/app/modules/admin/groups/form-page/group-form-page.component.html
@@ -0,0 +1,31 @@
+
+
+
+ {{ 'common.group' | translate }}
+ {{
+ (isUpdate ? 'sidebar.header.update' : 'sidebar.header.create')
+ | translate
+ }}
+
+
+
+
+
+
{{ 'common.id' | translate }}
+
+
+
+
{{ 'common.name' | translate }}
+
+
+
+
diff --git a/web/src/app/modules/admin/groups/form-page/group-form-page.component.scss b/web/src/app/modules/admin/groups/form-page/group-form-page.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/web/src/app/modules/admin/groups/form-page/group-form-page.component.spec.ts b/web/src/app/modules/admin/groups/form-page/group-form-page.component.spec.ts
new file mode 100644
index 0000000..a4a6c84
--- /dev/null
+++ b/web/src/app/modules/admin/groups/form-page/group-form-page.component.spec.ts
@@ -0,0 +1,50 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component';
+import { SharedModule } from 'src/app/modules/shared/shared.module';
+import { TranslateModule } from '@ngx-translate/core';
+import { AuthService } from 'src/app/service/auth.service';
+import { ErrorHandlingService } from 'src/app/service/error-handling.service';
+import { ToastService } from 'src/app/service/toast.service';
+import { ConfirmationService, MessageService } from 'primeng/api';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ApiKeyFormpageComponent', () => {
+ let component: RoleFormPageComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RoleFormPageComponent],
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ TranslateModule.forRoot(),
+ ],
+ providers: [
+ AuthService,
+ ErrorHandlingService,
+ ToastService,
+ MessageService,
+ ConfirmationService,
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ snapshot: { params: of({}) },
+ },
+ },
+ ApiKeysDataService,
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RoleFormPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/modules/admin/groups/form-page/group-form-page.component.ts b/web/src/app/modules/admin/groups/form-page/group-form-page.component.ts
new file mode 100644
index 0000000..e86ca33
--- /dev/null
+++ b/web/src/app/modules/admin/groups/form-page/group-form-page.component.ts
@@ -0,0 +1,93 @@
+import { Component } from "@angular/core";
+import { FormControl, FormGroup, Validators } from "@angular/forms";
+import { ToastService } from "src/app/service/toast.service";
+import { FormPageBase } from "src/app/core/base/form-page-base";
+import {
+ Group,
+ GroupCreateInput,
+ GroupUpdateInput,
+} from "src/app/model/entities/group";
+import { GroupsDataService } from "src/app/modules/admin/groups/groups.data.service";
+
+@Component({
+ selector: "app-group-form-page",
+ templateUrl: "./group-form-page.component.html",
+ styleUrl: "./group-form-page.component.scss",
+})
+export class GroupFormPageComponent extends FormPageBase<
+ Group,
+ GroupCreateInput,
+ GroupUpdateInput,
+ GroupsDataService
+> {
+ constructor(private toast: ToastService) {
+ super();
+ if (!this.nodeId) {
+ this.node = this.new();
+ this.setForm(this.node);
+
+ return;
+ }
+
+ this.dataService
+ .load([{ id: { equal: this.nodeId } }])
+ .subscribe((apiKey) => {
+ this.node = apiKey.nodes[0];
+ this.setForm(this.node);
+ });
+ }
+
+ new(): Group {
+ return {} as Group;
+ }
+
+ buildForm() {
+ this.form = new FormGroup({
+ id: new FormControl(undefined),
+ name: new FormControl(undefined, Validators.required),
+ });
+ this.form.controls["id"].disable();
+ }
+
+ setForm(node?: Group) {
+ this.form.controls["id"].setValue(node?.id);
+ this.form.controls["name"].setValue(node?.name);
+ }
+
+ getCreateInput(): GroupCreateInput {
+ return {
+ name: this.form.controls["name"].pristine
+ ? undefined
+ : (this.form.controls["name"].value ?? undefined),
+ };
+ }
+
+ getUpdateInput(): GroupUpdateInput {
+ if (!this.node?.id) {
+ throw new Error("Node id is missing");
+ }
+
+ return {
+ id: this.form.controls["id"].value,
+ name: this.form.controls["name"].pristine
+ ? undefined
+ : (this.form.controls["name"].value ?? undefined),
+ };
+ }
+
+ create(apiKey: GroupCreateInput): void {
+ this.dataService.create(apiKey).subscribe(() => {
+ this.spinner.hide();
+ this.toast.success("action.created");
+ this.close();
+ });
+ }
+
+ update(apiKey: GroupUpdateInput): void {
+ this.dataService.update(apiKey).subscribe(() => {
+ this.spinner.hide();
+ this.toast.success("action.created");
+ this.close();
+ });
+ }
+}
diff --git a/web/src/app/modules/admin/groups/groups.columns.ts b/web/src/app/modules/admin/groups/groups.columns.ts
new file mode 100644
index 0000000..20cc3c0
--- /dev/null
+++ b/web/src/app/modules/admin/groups/groups.columns.ts
@@ -0,0 +1,35 @@
+import { Injectable, Provider } from "@angular/core";
+import {
+ DB_MODEL_COLUMNS,
+ ID_COLUMN,
+ PageColumns,
+} from "src/app/core/base/page.columns";
+import { TableColumn } from "src/app/modules/shared/components/table/table.model";
+import { Group } from "src/app/model/entities/group";
+
+@Injectable()
+export class GroupsColumns extends PageColumns {
+ get(): TableColumn[] {
+ return [
+ ID_COLUMN,
+ {
+ name: "name",
+ label: "common.name",
+ type: "text",
+ filterable: true,
+ value: (row: Group) => row.name,
+ },
+ ...DB_MODEL_COLUMNS,
+ ];
+ }
+
+ static provide(): Provider[] {
+ return [
+ {
+ provide: PageColumns,
+ useClass: GroupsColumns,
+ },
+ GroupsColumns,
+ ];
+ }
+}
diff --git a/web/src/app/modules/admin/groups/groups.data.service.ts b/web/src/app/modules/admin/groups/groups.data.service.ts
new file mode 100644
index 0000000..d2620e0
--- /dev/null
+++ b/web/src/app/modules/admin/groups/groups.data.service.ts
@@ -0,0 +1,232 @@
+import { Injectable, Provider } from "@angular/core";
+import { Observable } from "rxjs";
+import {
+ Create,
+ Delete,
+ PageDataService,
+ Restore,
+ Update,
+} from "src/app/core/base/page.data.service";
+import { Filter } from "src/app/model/graphql/filter/filter.model";
+import { Sort } from "src/app/model/graphql/filter/sort.model";
+import { Apollo, gql } from "apollo-angular";
+import { QueryResult } from "src/app/model/entities/query-result";
+import { DB_MODEL_FRAGMENT } from "src/app/model/graphql/db-model.query";
+import { catchError, map } from "rxjs/operators";
+import { SpinnerService } from "src/app/service/spinner.service";
+import {
+ Group,
+ GroupCreateInput,
+ GroupUpdateInput,
+} from "src/app/model/entities/group";
+
+@Injectable()
+export class GroupsDataService
+ extends PageDataService
+ implements
+ Create,
+ Update,
+ Delete,
+ Restore
+{
+ constructor(
+ private spinner: SpinnerService,
+ private apollo: Apollo,
+ ) {
+ super();
+ }
+
+ load(
+ filter?: Filter[] | undefined,
+ sort?: Sort[] | undefined,
+ skip?: number | undefined,
+ take?: number | undefined,
+ ): Observable> {
+ return this.apollo
+ .query<{ groups: QueryResult }>({
+ query: gql`
+ query getGroups(
+ $filter: [GroupFilter]
+ $sort: [GroupSort]
+ $skip: Int
+ $take: Int
+ ) {
+ groups(filter: $filter, sort: $sort, skip: $skip, take: $take) {
+ count
+ totalCount
+ nodes {
+ id
+ name
+
+ ...DB_MODEL
+ }
+ }
+ }
+
+ ${DB_MODEL_FRAGMENT}
+ `,
+ variables: {
+ filter: filter,
+ sort: sort,
+ skip: skip,
+ take: take,
+ },
+ })
+ .pipe(
+ catchError((err) => {
+ this.spinner.hide();
+ throw err;
+ }),
+ )
+ .pipe(map((result) => result.data.groups));
+ }
+
+ loadById(id: number): Observable {
+ return this.apollo
+ .query<{ groups: QueryResult }>({
+ query: gql`
+ query getGroup($id: Int) {
+ group(filter: { id: { equal: $id } }) {
+ id
+ name
+
+ ...DB_MODEL
+ }
+ }
+
+ ${DB_MODEL_FRAGMENT}
+ `,
+ variables: {
+ id: id,
+ },
+ })
+ .pipe(
+ catchError((err) => {
+ this.spinner.hide();
+ throw err;
+ }),
+ )
+ .pipe(map((result) => result.data.groups.nodes[0]));
+ }
+
+ create(object: GroupCreateInput): Observable {
+ return this.apollo
+ .mutate<{ group: { create: Group } }>({
+ mutation: gql`
+ mutation createGroup($input: GroupCreateInput!) {
+ group {
+ create(input: $input) {
+ id
+ name
+
+ ...DB_MODEL
+ }
+ }
+ }
+
+ ${DB_MODEL_FRAGMENT}
+ `,
+ variables: {
+ input: {
+ name: object.name,
+ },
+ },
+ })
+ .pipe(
+ catchError((err) => {
+ this.spinner.hide();
+ throw err;
+ }),
+ )
+ .pipe(map((result) => result.data?.group.create));
+ }
+
+ update(object: GroupUpdateInput): Observable {
+ return this.apollo
+ .mutate<{ group: { update: Group } }>({
+ mutation: gql`
+ mutation updateGroup($input: GroupUpdateInput!) {
+ group {
+ update(input: $input) {
+ id
+ name
+
+ ...DB_MODEL
+ }
+ }
+ }
+
+ ${DB_MODEL_FRAGMENT}
+ `,
+ variables: {
+ input: {
+ id: object.id,
+ name: object.name,
+ },
+ },
+ })
+ .pipe(
+ catchError((err) => {
+ this.spinner.hide();
+ throw err;
+ }),
+ )
+ .pipe(map((result) => result.data?.group.update));
+ }
+
+ delete(object: Group): Observable {
+ return this.apollo
+ .mutate<{ group: { delete: boolean } }>({
+ mutation: gql`
+ mutation deleteGroup($id: ID!) {
+ group {
+ delete(id: $id)
+ }
+ }
+ `,
+ variables: {
+ id: object.id,
+ },
+ })
+ .pipe(
+ catchError((err) => {
+ this.spinner.hide();
+ throw err;
+ }),
+ )
+ .pipe(map((result) => result.data?.group.delete ?? false));
+ }
+
+ restore(object: Group): Observable {
+ return this.apollo
+ .mutate<{ group: { restore: boolean } }>({
+ mutation: gql`
+ mutation restoreGroup($id: ID!) {
+ group {
+ restore(id: $id)
+ }
+ }
+ `,
+ variables: {
+ id: object.id,
+ },
+ })
+ .pipe(
+ catchError((err) => {
+ this.spinner.hide();
+ throw err;
+ }),
+ )
+ .pipe(map((result) => result.data?.group.restore ?? false));
+ }
+
+ static provide(): Provider[] {
+ return [
+ {
+ provide: PageDataService,
+ useClass: GroupsDataService,
+ },
+ GroupsDataService,
+ ];
+ }
+}
diff --git a/web/src/app/modules/admin/groups/groups.module.ts b/web/src/app/modules/admin/groups/groups.module.ts
new file mode 100644
index 0000000..5fbdb22
--- /dev/null
+++ b/web/src/app/modules/admin/groups/groups.module.ts
@@ -0,0 +1,43 @@
+import { NgModule } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { SharedModule } from "src/app/modules/shared/shared.module";
+import { RouterModule, Routes } from "@angular/router";
+import { PermissionGuard } from "src/app/core/guard/permission.guard";
+import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
+import { GroupsPage } from "src/app/modules/admin/groups/groups.page";
+import { GroupFormPageComponent } from "src/app/modules/admin/groups/form-page/group-form-page.component";
+import { GroupsDataService } from "src/app/modules/admin/groups/groups.data.service";
+import { GroupsColumns } from "src/app/modules/admin/groups/groups.columns";
+
+const routes: Routes = [
+ {
+ path: "",
+ title: "Groups",
+ component: GroupsPage,
+ children: [
+ {
+ path: "create",
+ component: GroupFormPageComponent,
+ canActivate: [PermissionGuard],
+ data: {
+ permissions: [PermissionsEnum.apiKeysCreate],
+ },
+ },
+ {
+ path: "edit/:id",
+ component: GroupFormPageComponent,
+ canActivate: [PermissionGuard],
+ data: {
+ permissions: [PermissionsEnum.apiKeysUpdate],
+ },
+ },
+ ],
+ },
+];
+
+@NgModule({
+ declarations: [GroupsPage, GroupFormPageComponent],
+ imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
+ providers: [GroupsDataService.provide(), GroupsColumns.provide()],
+})
+export class GroupsModule {}
diff --git a/web/src/app/modules/admin/groups/groups.page.html b/web/src/app/modules/admin/groups/groups.page.html
new file mode 100644
index 0000000..d9a1c72
--- /dev/null
+++ b/web/src/app/modules/admin/groups/groups.page.html
@@ -0,0 +1,19 @@
+
+
+
diff --git a/web/src/app/modules/admin/groups/groups.page.scss b/web/src/app/modules/admin/groups/groups.page.scss
new file mode 100644
index 0000000..e69de29
diff --git a/web/src/app/modules/admin/groups/groups.page.spec.ts b/web/src/app/modules/admin/groups/groups.page.spec.ts
new file mode 100644
index 0000000..98eb39d
--- /dev/null
+++ b/web/src/app/modules/admin/groups/groups.page.spec.ts
@@ -0,0 +1,51 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page';
+import { SharedModule } from 'src/app/modules/shared/shared.module';
+import { TranslateModule } from '@ngx-translate/core';
+import { AuthService } from 'src/app/service/auth.service';
+import { KeycloakService } from 'keycloak-angular';
+import { ErrorHandlingService } from 'src/app/service/error-handling.service';
+import { ToastService } from 'src/app/service/toast.service';
+import { ConfirmationService, MessageService } from 'primeng/api';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+import { PageDataService } from 'src/app/core/base/page.data.service';
+import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
+
+describe('ApiKeysComponent', () => {
+ let component: ApiKeysPage;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ApiKeysPage],
+ imports: [SharedModule, TranslateModule.forRoot()],
+ providers: [
+ AuthService,
+ KeycloakService,
+ ErrorHandlingService,
+ ToastService,
+ MessageService,
+ ConfirmationService,
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ snapshot: { params: of({}) },
+ },
+ },
+ {
+ provide: PageDataService,
+ useClass: ApiKeysDataService,
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ApiKeysPage);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/src/app/modules/admin/groups/groups.page.ts b/web/src/app/modules/admin/groups/groups.page.ts
new file mode 100644
index 0000000..f6a835e
--- /dev/null
+++ b/web/src/app/modules/admin/groups/groups.page.ts
@@ -0,0 +1,72 @@
+import { Component } from "@angular/core";
+import { PageBase } from "src/app/core/base/page-base";
+import { ToastService } from "src/app/service/toast.service";
+import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
+import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
+import { Group } from "src/app/model/entities/group";
+import { GroupsDataService } from "src/app/modules/admin/groups/groups.data.service";
+import { GroupsColumns } from "src/app/modules/admin/groups/groups.columns";
+
+@Component({
+ selector: "app-groups",
+ templateUrl: "./groups.page.html",
+ styleUrl: "./groups.page.scss",
+})
+export class GroupsPage extends PageBase<
+ Group,
+ GroupsDataService,
+ GroupsColumns
+> {
+ constructor(
+ private toast: ToastService,
+ private confirmation: ConfirmationDialogService,
+ ) {
+ super(true, {
+ read: [PermissionsEnum.groups],
+ create: [PermissionsEnum.groupsCreate],
+ update: [PermissionsEnum.groupsUpdate],
+ delete: [PermissionsEnum.groupsDelete],
+ restore: [PermissionsEnum.groupsDelete],
+ });
+ }
+
+ load(): void {
+ this.loading = true;
+ this.dataService
+ .load(this.filter, this.sort, this.skip, this.take)
+ .subscribe((result) => {
+ this.result = result;
+ this.loading = false;
+ });
+ }
+
+ delete(group: Group): void {
+ this.confirmation.confirmDialog({
+ header: "dialog.delete.header",
+ message: "dialog.delete.message",
+ accept: () => {
+ this.loading = true;
+ this.dataService.delete(group).subscribe(() => {
+ this.toast.success("action.deleted");
+ this.load();
+ });
+ },
+ messageParams: { entity: group.name },
+ });
+ }
+
+ restore(group: Group): void {
+ this.confirmation.confirmDialog({
+ header: "dialog.restore.header",
+ message: "dialog.restore.message",
+ accept: () => {
+ this.loading = true;
+ this.dataService.restore(group).subscribe(() => {
+ this.toast.success("action.restored");
+ this.load();
+ });
+ },
+ messageParams: { entity: group.name },
+ });
+ }
+}
diff --git a/web/src/app/service/auth.service.ts b/web/src/app/service/auth.service.ts
index 0b9d920..c4a939d 100644
--- a/web/src/app/service/auth.service.ts
+++ b/web/src/app/service/auth.service.ts
@@ -1,30 +1,29 @@
-import { Injectable } from '@angular/core';
-import { User } from 'src/app/model/auth/user';
-import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
-import { Apollo, gql } from 'apollo-angular';
-import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
-import { KeycloakService } from 'keycloak-angular';
-import { map } from 'rxjs/operators';
-import { Logger } from 'src/app/service/logger.service';
+import { Injectable } from "@angular/core";
+import { User } from "src/app/model/auth/user";
+import { BehaviorSubject, firstValueFrom, Observable } from "rxjs";
+import { Apollo, gql } from "apollo-angular";
+import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
+import { KeycloakService } from "keycloak-angular";
+import { map } from "rxjs/operators";
+import { Logger } from "src/app/service/logger.service";
-const log = new Logger('AuthService');
+const log = new Logger("AuthService");
@Injectable({
- providedIn: 'root',
+ providedIn: "root",
})
export class AuthService {
protected anyPermissionForAdminPage = [
PermissionsEnum.apiKeys,
PermissionsEnum.users,
PermissionsEnum.roles,
- PermissionsEnum.news,
];
user$ = new BehaviorSubject(null);
constructor(
private apollo: Apollo,
- private keycloakService: KeycloakService
+ private keycloakService: KeycloakService,
) {}
private requestUser() {
@@ -61,11 +60,11 @@ export class AuthService {
permission,
},
})
- .pipe(map(result => result.data.userHasPermission));
+ .pipe(map((result) => result.data.userHasPermission));
}
private userHasAnyPermission(
- permissions: PermissionsEnum[]
+ permissions: PermissionsEnum[],
): Observable {
return this.apollo
.query<{ userHasAnyPermission: boolean }>({
@@ -78,7 +77,7 @@ export class AuthService {
permissions,
},
})
- .pipe(map(result => result.data.userHasAnyPermission));
+ .pipe(map((result) => result.data.userHasAnyPermission));
}
loadUser() {
@@ -89,8 +88,8 @@ export class AuthService {
return;
}
- this.requestUser().subscribe(result => {
- log.info('User loaded');
+ this.requestUser().subscribe((result) => {
+ log.info("User loaded");
this.user$.next(result.data.user);
});
}
@@ -112,10 +111,10 @@ export class AuthService {
if (!this.user$.value) return false;
const userPermissions = this.user$.value.roles
- .map(role => (role.permissions ?? []).map(p => p.name))
+ .map((role) => (role.permissions ?? []).map((p) => p.name))
.flat();
- return permissions.every(permission =>
- userPermissions.includes(permission)
+ return permissions.every((permission) =>
+ userPermissions.includes(permission),
);
}
@@ -131,7 +130,7 @@ export class AuthService {
if (!this.user$.value) return false;
const permissions = this.user$.value.roles
- .map(role => (role.permissions ?? []).map(p => p.name))
+ .map((role) => (role.permissions ?? []).map((p) => p.name))
.flat();
return permissions.includes(permission);
}
diff --git a/web/src/app/service/error-handling.service.ts b/web/src/app/service/error-handling.service.ts
index d035d0b..2fce807 100644
--- a/web/src/app/service/error-handling.service.ts
+++ b/web/src/app/service/error-handling.service.ts
@@ -29,7 +29,8 @@ export class ErrorHandlingService implements ErrorHandler {
this.handleHttpError(error.networkError as HttpErrorResponse);
return;
}
- this.handleHttpError(error);
+ console.error(error);
+ // this.handleHttpError(error);
}
private handleHttpError(e: HttpErrorResponse) {
diff --git a/web/src/app/service/sidebar.service.ts b/web/src/app/service/sidebar.service.ts
index ef1de6d..60b9310 100644
--- a/web/src/app/service/sidebar.service.ts
+++ b/web/src/app/service/sidebar.service.ts
@@ -1,99 +1,83 @@
-import { Injectable } from '@angular/core';
-import { BehaviorSubject } from 'rxjs';
-import { MenuElement } from 'src/app/model/view/menu-element';
-import { Router } from '@angular/router';
-import { AuthService } from 'src/app/service/auth.service';
-import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
+import {Injectable} from '@angular/core';
+import {BehaviorSubject} from 'rxjs';
+import {MenuElement} from 'src/app/model/view/menu-element';
+import {AuthService} from 'src/app/service/auth.service';
+import {PermissionsEnum} from 'src/app/model/auth/permissionsEnum';
@Injectable({
- providedIn: 'root',
+ providedIn: 'root',
})
export class SidebarService {
- visible$ = new BehaviorSubject(false);
- elements$ = new BehaviorSubject([]);
+ visible$ = new BehaviorSubject(true);
+ elements$ = new BehaviorSubject([]);
- constructor(
- private router: Router,
- private auth: AuthService
- ) {
- this.router.events.subscribe(() => {
- if (this.router.url.startsWith('/admin')) {
- this.show();
- } else {
- this.hide();
- }
- });
+ constructor(
+ private auth: AuthService
+ ) {
+ this.auth.user$.subscribe(user => {
+ if (user) {
+ this.setElements().then();
+ }
+ });
+ }
- this.auth.user$.subscribe(user => {
- if (user) {
- this.setElements().then();
- }
- });
- }
+ show() {
+ this.visible$.next(true);
+ }
- show() {
- this.visible$.next(true);
- }
+ hide() {
+ this.visible$.next(false);
+ }
- hide() {
- this.visible$.next(false);
- }
+ // trust me, you'll need this async
+ async setElements() {
+ const elements: MenuElement[] = [
+ {
+ label: 'common.groups',
+ icon: 'pi pi-tags',
+ routerLink: ['/admin/groups'],
+ },
+ {
+ label: 'common.urls',
+ icon: 'pi pi-tag',
+ routerLink: ['/admin/urls'],
+ },
+ await this.groupAdministration(),
+ ];
+ this.elements$.next(elements);
+ }
- // trust me, you'll need this async
- async setElements() {
- const elements: MenuElement[] = [
- await this.groupPublic(),
- await this.groupAdministration(),
- ];
- this.elements$.next(elements);
- }
-
- async groupPublic() {
- return {
- label: 'sidebar.public',
- icon: 'pi pi-globe',
- expanded: true,
- items: [
- {
- label: 'common.news',
- icon: 'pi pi-file',
- routerLink: ['/admin/public/news'],
- },
- ],
- };
- }
-
- async groupAdministration() {
- return {
- label: 'sidebar.administration',
- icon: 'pi pi-wrench',
- expanded: true,
- items: [
- {
- label: 'sidebar.users',
- icon: 'pi pi-user',
- routerLink: ['/admin/administration/users'],
- visible: await this.auth.hasAnyPermissionLazy([
- PermissionsEnum.users,
- ]),
- },
- {
- label: 'sidebar.roles',
- icon: 'pi pi-user-edit',
- routerLink: ['/admin/administration/roles'],
- visible: await this.auth.hasAnyPermissionLazy([
- PermissionsEnum.roles,
- ]),
- },
- {
- label: 'sidebar.api_keys',
- icon: 'pi pi-key',
- routerLink: ['/admin/administration/api-keys'],
- visible: await this.auth.hasAnyPermissionLazy([
- PermissionsEnum.apiKeys,
- ]),
- },
- ],
- };
- }
+ async groupAdministration() {
+ return {
+ label: 'sidebar.administration',
+ icon: 'pi pi-wrench',
+ expanded: true,
+ items: [
+ {
+ label: 'sidebar.users',
+ icon: 'pi pi-user',
+ routerLink: ['/admin/administration/users'],
+ visible: await this.auth.hasAnyPermissionLazy([
+ PermissionsEnum.users,
+ ]),
+ },
+ {
+ label: 'sidebar.roles',
+ icon: 'pi pi-user-edit',
+ routerLink: ['/admin/administration/roles'],
+ visible: await this.auth.hasAnyPermissionLazy([
+ PermissionsEnum.roles,
+ ]),
+ },
+ {
+ label: 'sidebar.api_keys',
+ icon: 'pi pi-key',
+ routerLink: ['/admin/administration/api-keys'],
+ visible: await this.auth.hasAnyPermissionLazy([
+ PermissionsEnum.apiKeys,
+ ]),
+ },
+ ],
+ };
+ }
}
diff --git a/web/src/environments/environment.local.ts b/web/src/environments/environment.local.ts
index 1d0a1d5..b5e169f 100644
--- a/web/src/environments/environment.local.ts
+++ b/web/src/environments/environment.local.ts
@@ -4,10 +4,15 @@
export const environment = {
production: false,
+ // auth: {
+ // url: 'https://auth.sh-edraft.de',
+ // realm: 'dev',
+ // clientId: 'open-redirect-client',
+ // },
auth: {
- url: 'https://auth.sh-edraft.de',
- realm: 'dev',
- clientId: 'open-redirect-client',
+ url: 'https://keycloak.maxlan.de',
+ realm: 'develop',
+ clientId: 'lan-maestro-client-local',
},
api: {
url: 'http://localhost:5000/api',