Editable groups

This commit is contained in:
Sven Heidemann 2024-12-14 12:54:30 +01:00
parent 565f21429a
commit bb44e59a14
31 changed files with 1101 additions and 177 deletions

View File

@ -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
}

View File

@ -3,4 +3,7 @@ type Mutation {
user: UserMutation
role: RoleMutation
group: GroupMutation
shortUrl: ShortUrlMutation
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
],
)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -10,10 +10,6 @@
</div>
</div>
<div class="flex items-center justify-center w-1/3" *ngIf="menu.length > 0">
<app-menu-bar class="w-full" [elements]="menu"></app-menu-bar>
</div>
<div class="flex items-center justify-center">
<div class="flex items-center justify-center">
<p-button

View File

@ -1,15 +1,15 @@
import { Component } from '@angular/core';
import { MenuElement } from 'src/app/model/view/menu-element';
import { Subject } from 'rxjs';
import { SidebarService } from 'src/app/service/sidebar.service';
import { takeUntil } from 'rxjs/operators';
import {Component, OnDestroy} from '@angular/core';
import {MenuElement} from 'src/app/model/view/menu-element';
import {Subject} from 'rxjs';
import {SidebarService} from 'src/app/service/sidebar.service';
import {takeUntil} from 'rxjs/operators';
@Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.scss',
})
export class SidebarComponent {
export class SidebarComponent implements OnDestroy {
elements: MenuElement[] = [];
unsubscribe$ = new Subject<void>();
@ -21,4 +21,9 @@ export class SidebarComponent {
this.elements = elements;
});
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}

View File

@ -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",
}

View File

@ -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;
}

View File

@ -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,
),
},
];

View File

@ -0,0 +1,31 @@
<app-form-page
*ngIf="node"
[formGroup]="form"
[isUpdate]="isUpdate"
(onSave)="save()"
(onClose)="close()">
<ng-template formPageHeader let-isUpdate>
<h2>
{{ 'common.group' | translate }}
{{
(isUpdate ? 'sidebar.header.update' : 'sidebar.header.create')
| translate
}}
</h2>
</ng-template>
<ng-template formPageContent>
<div class="form-page-input">
<p class="label">{{ 'common.id' | translate }}</p>
<input pInputText class="value" type="number" formControlName="id"/>
</div>
<div class="form-page-input">
<p class="label">{{ 'common.name' | translate }}</p>
<input
pInputText
class="value"
type="text"
formControlName="name"/>
</div>
</ng-template>
</app-form-page>

View File

@ -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<RoleFormPageComponent>;
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();
});
});

View File

@ -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<number | undefined>(undefined),
name: new FormControl<string | undefined>(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();
});
}
}

View File

@ -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<Group> {
get(): TableColumn<Group>[] {
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,
];
}
}

View File

@ -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<Group>
implements
Create<Group, GroupCreateInput>,
Update<Group, GroupUpdateInput>,
Delete<Group>,
Restore<Group>
{
constructor(
private spinner: SpinnerService,
private apollo: Apollo,
) {
super();
}
load(
filter?: Filter[] | undefined,
sort?: Sort[] | undefined,
skip?: number | undefined,
take?: number | undefined,
): Observable<QueryResult<Group>> {
return this.apollo
.query<{ groups: QueryResult<Group> }>({
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<Group> {
return this.apollo
.query<{ groups: QueryResult<Group> }>({
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<Group | undefined> {
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<Group | undefined> {
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<boolean> {
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<boolean> {
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,
];
}
}

View File

@ -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 {}

View File

@ -0,0 +1,19 @@
<app-table
[rows]="result.nodes"
[columns]="columns"
[rowsPerPageOptions]="rowsPerPageOptions"
[totalCount]="result.totalCount"
[requireAnyPermissions]="requiredPermissions"
countHeaderTranslation="group.count_header"
[loading]="loading"
[(filter)]="filter"
[(sort)]="sort"
[(skip)]="skip"
[(take)]="take"
(load)="load()"
[create]="true"
[update]="true"
(delete)="delete($event)"
(restore)="restore($event)"></app-table>
<router-outlet></router-outlet>

View File

@ -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<ApiKeysPage>;
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();
});
});

View File

@ -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 },
});
}
}

View File

@ -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<User | null>(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<boolean> {
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);
}

View File

@ -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) {

View File

@ -1,29 +1,19 @@
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',
})
export class SidebarService {
visible$ = new BehaviorSubject<boolean>(false);
visible$ = new BehaviorSubject<boolean>(true);
elements$ = new BehaviorSubject<MenuElement[]>([]);
constructor(
private router: Router,
private auth: AuthService
) {
this.router.events.subscribe(() => {
if (this.router.url.startsWith('/admin')) {
this.show();
} else {
this.hide();
}
});
this.auth.user$.subscribe(user => {
if (user) {
this.setElements().then();
@ -42,27 +32,21 @@ export class SidebarService {
// trust me, you'll need this async
async setElements() {
const elements: MenuElement[] = [
await this.groupPublic(),
{
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);
}
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',

View File

@ -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',