Fixed some issues
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 38s
Test before pr merge / test-translation-lint (pull_request) Successful in 1m11s
Test before pr merge / test-before-merge (pull_request) Failing after 1m48s

This commit is contained in:
Sven Heidemann 2025-03-08 09:49:24 +01:00
parent 9f68929b34
commit 86bd5fb545
15 changed files with 401 additions and 89 deletions

View File

@ -13,11 +13,11 @@ class MutationABC(QueryABC):
QueryABC.__init__(self, f"{name}Mutation")
def add_mutation_type(
self,
name: str,
mutation_name: str,
require_any_permission: list[Permissions] = None,
public: bool = False,
self,
name: str,
mutation_name: str,
require_any_permission=None,
public: bool = False,
):
"""
Add mutation type (sub mutation) to the mutation object
@ -27,6 +27,8 @@ class MutationABC(QueryABC):
:param bool public: Define if the field can resolve without authentication
:return:
"""
if require_any_permission is None:
require_any_permission = []
from api_graphql.definition import QUERIES
self.field(

View File

@ -7,4 +7,8 @@ type Mutation {
group: GroupMutation
domain: DomainMutation
shortUrl: ShortUrlMutation
setting: SettingMutation
userSetting: UserSettingMutation
featureFlag: FeatureFlagMutation
}

View File

@ -0,0 +1,18 @@
from api_graphql.abc.input_abc import InputABC
class FeatureFlagInput(InputABC):
def __init__(self, src: dict):
InputABC.__init__(self, src)
self._key = self.option("key", str, required=True)
self._value = self.option("value", bool, required=True)
@property
def key(self) -> str:
return self._key
@property
def value(self) -> bool:
return self._value

View File

@ -0,0 +1,18 @@
from api_graphql.abc.input_abc import InputABC
class SettingInput(InputABC):
def __init__(self, src: dict):
InputABC.__init__(self, src)
self._key = self.option("key", str, required=True)
self._value = self.option("value", str, required=True)
@property
def key(self) -> str:
return self._key
@property
def value(self) -> str:
return self._value

View File

@ -0,0 +1,18 @@
from api_graphql.abc.input_abc import InputABC
class UserSettingInput(InputABC):
def __init__(self, src: dict):
InputABC.__init__(self, src)
self._key = self.option("key", str, required=True)
self._value = self.option("value", str, required=True)
@property
def key(self) -> str:
return self._key
@property
def value(self) -> str:
return self._value

View File

@ -60,3 +60,22 @@ class Mutation(MutationABC):
Permissions.short_urls_delete,
],
)
self.add_mutation_type(
"setting",
"Setting",
require_any_permission=[
Permissions.settings_update,
],
)
self.add_mutation_type(
"userSetting",
"UserSetting",
)
self.add_mutation_type(
"featureFlag",
"FeatureFlag",
require_any_permission=[
Permissions.administrator,
],
)

View File

@ -0,0 +1,32 @@
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.feature_flag_input import FeatureFlagInput
from core.logger import APILogger
from data.schemas.system.feature_flag import FeatureFlag
from data.schemas.system.feature_flag_dao import featureFlagDao
from service.permission.permissions_enum import Permissions
logger = APILogger(__name__)
class FeatureFlagMutation(MutationABC):
def __init__(self):
MutationABC.__init__(self, "FeatureFlag")
self.mutation(
"change",
self.resolve_change,
FeatureFlagInput,
require_any_permission=[Permissions.administrator],
)
@staticmethod
async def resolve_change(obj: FeatureFlagInput, *_):
logger.debug(f"create new feature flag: {input}")
setting = await featureFlagDao.find_single_by({FeatureFlag.key: obj.key})
if setting is None:
raise ValueError(f"FeatureFlag {obj.key} not found")
setting.value = obj.value
await featureFlagDao.update(setting)
return await featureFlagDao.get_by_id(setting.id)

View File

@ -0,0 +1,32 @@
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.setting_input import SettingInput
from core.logger import APILogger
from data.schemas.system.setting import Setting
from data.schemas.system.setting_dao import settingsDao
from service.permission.permissions_enum import Permissions
logger = APILogger(__name__)
class SettingMutation(MutationABC):
def __init__(self):
MutationABC.__init__(self, "Setting")
self.mutation(
"change",
self.resolve_change,
SettingInput,
require_any_permission=[Permissions.settings_update],
)
@staticmethod
async def resolve_change(obj: SettingInput, *_):
logger.debug(f"create new setting: {input}")
setting = await settingsDao.find_single_by({Setting.key: obj.key})
if setting is None:
raise ValueError(f"Setting with key {obj.key} not found")
setting.value = obj.value
await settingsDao.update(setting)
return await settingsDao.get_by_id(setting.id)

View File

@ -0,0 +1,40 @@
from api.route import Route
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.user_setting_input import UserSettingInput
from core.logger import APILogger
from data.schemas.public.user_setting import UserSetting
from data.schemas.public.user_setting_dao import userSettingsDao
from data.schemas.system.setting_dao import settingsDao
from service.permission.permissions_enum import Permissions
logger = APILogger(__name__)
class UserSettingMutation(MutationABC):
def __init__(self):
MutationABC.__init__(self, "UserSetting")
self.mutation(
"change",
self.resolve_change,
UserSettingInput,
require_any_permission=[Permissions.settings_update],
)
@staticmethod
async def resolve_change(obj: UserSettingInput, *_):
logger.debug(f"create new setting: {input}")
user = await Route.get_user_or_default()
if user is None:
logger.debug("user not authorized")
return None
setting = await userSettingsDao.find_single_by(
[{UserSetting.user_id: user.id}, {UserSetting.key: obj.key}]
)
if setting is None:
await userSettingsDao.create(UserSetting(0, user.id, obj.key, obj.value))
else:
setting.value = obj.value
await userSettingsDao.update(setting)
return await userSettingsDao.find_by_key(user, obj.key)

View File

@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS public.user_settings
Id SERIAL PRIMARY KEY,
Key TEXT NOT NULL,
Value TEXT NOT NULL,
UserId INT NOT NULL REFERENCES public.user_settings (Id) ON DELETE CASCADE,
UserId INT NOT NULL REFERENCES administration.users (Id) ON DELETE CASCADE,
-- for history
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
EditorId INT NULL REFERENCES administration.users (Id),

View File

@ -8,8 +8,9 @@ import { User } from 'src/app/model/auth/user';
import { AuthService } from 'src/app/service/auth.service';
import { MenuElement } from 'src/app/model/view/menu-element';
import { SidebarService } from 'src/app/service/sidebar.service';
import { SettingsService } from 'src/app/service/settings.service';
import { ConfigService } from 'src/app/service/config.service';
import { UserSettingsService } from 'src/app/service/user_settings.service';
import { SettingsService } from 'src/app/service/settings.service';
@Component({
selector: 'app-header',
@ -33,7 +34,9 @@ export class HeaderComponent implements OnInit, OnDestroy {
private guiService: GuiService,
private auth: AuthService,
private sidebarService: SidebarService,
private config: ConfigService
private config: ConfigService,
private settings: SettingsService,
private userSettings: UserSettingsService
) {
this.guiService.isMobile$
.pipe(takeUntil(this.unsubscribe$))
@ -47,13 +50,18 @@ export class HeaderComponent implements OnInit, OnDestroy {
this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe(async user => {
this.user = user;
await this.initMenuLists();
if (user) {
await this.loadTheme();
await this.loadLang();
}
});
this.themeList = this.config.settings.themes.map(theme => {
return {
label: theme.label,
command: () => {
command: async () => {
this.guiService.theme$.next(theme.name);
await this.userSettings.set('theme', theme.name);
},
};
});
@ -61,7 +69,6 @@ export class HeaderComponent implements OnInit, OnDestroy {
async ngOnInit() {
await this.initMenuLists();
await this.loadLang();
}
ngOnDestroy() {
@ -74,24 +81,49 @@ export class HeaderComponent implements OnInit, OnDestroy {
}
async initMenuLists() {
await this.initMenuList();
await this.initLangMenuList();
await this.initUserMenuList();
}
async initMenuList() {
this.menu = [
{
label: 'common.news',
routerLink: ['/'],
icon: 'pi pi-home',
},
{
label: 'header.menu.about',
routerLink: ['/about'],
icon: 'pi pi-info',
},
];
if (this.auth.user$.value) {
this.menu.push({
label: 'header.menu.admin',
routerLink: ['/admin'],
icon: 'pi pi-cog',
visible: await this.auth.isAdmin(),
});
}
}
async initLangMenuList() {
this.langList = [
{
label: 'English',
command: () => {
command: async () => {
this.translate('en');
this.setLang('en');
await this.setLang('en');
},
},
{
label: 'Deutsch',
command: () => {
command: async () => {
this.translate('de');
this.setLang('de');
await this.setLang('de');
},
},
];
@ -109,10 +141,8 @@ export class HeaderComponent implements OnInit, OnDestroy {
},
{
label: this.translateService.instant('header.logout'),
command: () => {
this.auth.logout().then(() => {
console.log('logout');
});
command: async () => {
await this.auth.logout();
},
icon: 'pi pi-sign-out',
},
@ -126,15 +156,33 @@ export class HeaderComponent implements OnInit, OnDestroy {
.subscribe(res => this.ngConfig.setTranslation(res));
}
async loadLang() {
const lang = 'en';
this.setLang(lang);
this.translate(lang);
async loadTheme() {
const defaultTheme = (await this.settings.get('default_theme')) ?? 'maxlan';
const userTheme = await this.userSettings.get('theme');
const theme = userTheme ?? defaultTheme;
this.guiService.theme$.next(theme);
if (!userTheme) {
await this.userSettings.set('theme', theme);
}
}
setLang(lang: string) {
// this.settings.setSetting(`lang`, lang);
console.log('setLang', lang);
async loadLang() {
const defaultLang = (await this.settings.get('default_language')) ?? 'en';
const userLang = await this.userSettings.get('language');
const lang = userLang ?? defaultLang;
this.translate(lang);
if (userLang) {
return;
}
await this.userSettings.set('language', lang);
}
async setLang(lang: string) {
await this.userSettings.set('language', lang);
}
toggleSidebar() {

View File

@ -103,6 +103,14 @@ export class AuthService {
return this.keycloakService.logout();
}
isLoggedIn(): boolean {
return (
this.keycloakService.isLoggedIn() &&
!this.keycloakService.isTokenExpired() &&
!!this.user$.value
);
}
async isAdmin(): Promise<boolean> {
return await this.hasAnyPermissionLazy(this.anyPermissionForAdminPage);
}

View File

@ -0,0 +1,111 @@
import { Injectable } from '@angular/core';
import { throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Setting } from 'src/app/model/entities/setting';
import { Apollo, gql } from 'apollo-angular';
import { Logger } from 'src/app/service/logger.service';
import { AuthService } from 'src/app/service/auth.service';
const log = new Logger('UserSettings');
@Injectable({
providedIn: 'root',
})
export class UserSettingsService {
constructor(
private apollo: Apollo,
private auth: AuthService
) {}
get(key: string): Promise<string | undefined> {
if (!this.auth.isLoggedIn()) {
log.debug('get', key, 'not logged in');
return Promise.resolve(undefined);
}
log.debug('get', key);
return new Promise((resolve, reject) => {
this.apollo
.query<{ userSettings: Setting[] }>({
query: gql`
query getUserSetting($key: String!) {
userSettings(key: $key) {
key
value
}
}
`,
variables: {
key,
},
})
.pipe(
catchError(err => {
throw err;
})
)
.pipe(
map(result =>
result.data.userSettings?.length > 0
? result.data.userSettings[0]
: undefined
)
)
.pipe(
catchError(err => {
reject(err);
return throwError(() => err);
})
)
.subscribe(setting => {
log.debug('Got', key, setting);
resolve(setting?.value);
});
});
}
set(key: string, value: string): Promise<void> {
if (!this.auth.isLoggedIn()) {
log.debug('set', key, value, 'not logged in');
return Promise.resolve(undefined);
}
log.debug('set', key, value);
return new Promise((resolve, reject) => {
this.apollo
.mutate<{ userSetting: { change: Setting } }>({
mutation: gql`
mutation changeUserSetting($input: UserSettingInput!) {
userSetting {
change(input: $input) {
key
value
}
}
}
`,
variables: {
input: {
key: key,
value: value,
},
},
})
.pipe(
catchError(err => {
throw err;
})
)
.pipe(map(result => result.data?.userSetting.change))
.pipe(
catchError(err => {
reject(err);
return throwError(() => err);
})
)
.subscribe(() => {
resolve();
});
});
}
}

View File

@ -16,7 +16,6 @@
"all": "Alle",
"api_error": "API Fehler",
"api_key": "API Key",
"by": "von",
"cancel": "Abbrechen",
"choose": "Auswählen",
"close": "Schließen",
@ -28,7 +27,6 @@
"domain": "Domain",
"domains": "Domains",
"download": "Herunterladen",
"edited_at": "Bearbeitet am",
"editor": "Bearbeiter",
"error": "Fehler",
"group": "Gruppe",
@ -37,7 +35,6 @@
"identifier": "Identifier",
"key": "Schlüssel",
"name": "Name",
"news": "News",
"role": "Rolle",
"save": "Speichern",
"short_url": "Url",
@ -48,9 +45,7 @@
},
"dialog": {
"abort": "Abbrechen",
"back": "Zurück",
"confirm": "Bestätigen",
"continue": "Fortsetzen",
"delete": {
"header": "Löschen bestätigen",
"message": "Sind Sie sicher, dass Sie {{entity}} löschen möchten?"
@ -58,22 +53,18 @@
"restore": {
"header": "Wiederherstellung bestätigen",
"message": "Sind Sie sicher, dass Sie {{entity}} wiederherstellen möchten?"
},
"save": "Speichern"
}
},
"domain": {
"count_header": "Domain(s)"
},
"error": {
"404": "404 - Nicht gefunden",
"create_failed": "Erstellung fehlgeschlagen",
"delete_failed": "Löschen fehlgeschlagen",
"missing_permissions": "Fehlende Berechtigungen: {{permissions}}",
"network_error": "Netzwerkfehler",
"permission_denied": "Fehlende Berechtigung",
"restore_failed": "Wiederherstellung fehlgeschlagen",
"unauthorized": "Nicht autorisiert",
"update_failed": "Bearbeitung fehlgeschlagen"
"retry": "Erneut versuchen",
"server_unavailable": "Server nicht erreichbar",
"unauthorized": "Nicht autorisiert"
},
"footer": {
"imprint": "Impressum",
@ -84,11 +75,12 @@
"count_header": "Gruppe(n)"
},
"header": {
"logout": "Ausloggen",
"menu": {
"about": "Über uns",
"admin": "Admin"
}
"logout": "Ausloggen"
},
"permission_descriptions": {
"short_urls": "Alle URLs sehen",
"short_urls.by_assignment": "Alle Kurz-URLs anzeigen, die einer Gruppe nach Rolle zugewiesen sind",
"users.update": "Benutzer inkl. der Rollen ändern"
},
"permissions": {
"api_keys": "API Keys",
@ -107,10 +99,6 @@
"ip_list.create": "Erstellen",
"ip_list.delete": "Löschen",
"ip_list.update": "Bearbeiten",
"news": "News",
"news.create": "Erstellen",
"news.delete": "Löschen",
"news.update": "Bearbeiten",
"roles": "Rollen",
"roles.create": "Erstellen",
"roles.delete": "Löschen",
@ -125,19 +113,10 @@
"users.delete": "Löschen",
"users.update": "Bearbeiten"
},
"qr": {
"width": "Breite"
},
"permission_descriptions": {
"users.update": "Benutzer inkl. der Rollen ändern",
"short_urls": "Alle URLs sehen",
"short_urls.by_assignment": "Alle Kurz-URLs anzeigen, die einer Gruppe nach Rolle zugewiesen sind"
},
"role": {
"count_header": "Rolle(n)"
},
"short_url": {
"count_header": "Url(s)",
"loading_screen": "Ladebildschirm",
"short_url": "URL",
"target_url": "Ziel",
@ -146,12 +125,13 @@
"sidebar": {
"administration": "Administration",
"api_keys": "API Keys",
"feature_flags": "Funktionen",
"header": {
"create": "erstellen",
"update": "bearbeiten"
},
"public": "Öffentlich",
"roles": "Rollen",
"settings": "Einstellungen",
"users": "Benutzer"
},
"table": {
@ -163,6 +143,7 @@
"reset_filters": "Filter zurücksetzen",
"reset_sort": "Sortierung zurücksetzen",
"restore": "Wiederherstellen",
"select_columns": "Spalten auswählen",
"show_deleted": "Gelöschte anzeigen",
"to": "bis",
"update": "Bearbeiten"

View File

@ -16,7 +16,6 @@
"all": "All",
"api_error": "API error",
"api_key": "API Key",
"by": "by",
"cancel": "Cancel",
"choose": "Choose",
"close": "Close",
@ -28,7 +27,6 @@
"domain": "Domain",
"domains": "Domains",
"download": "Download",
"edited_at": "Edited at",
"editor": "Editor",
"error": "Fehler",
"group": "Group",
@ -37,7 +35,6 @@
"identifier": "Identifier",
"key": "Key",
"name": "Name",
"news": "News",
"role": "Role",
"save": "Save",
"short_url": "Url",
@ -48,9 +45,7 @@
},
"dialog": {
"abort": "Abort",
"back": "Back",
"confirm": "Confirm",
"continue": "Continue",
"delete": {
"header": "Confirm delete",
"message": "Are you sure you want to delete {{entity}}?"
@ -58,22 +53,18 @@
"restore": {
"header": "Confirm restore",
"message": "Are you sure you want to restore {{entity}}?"
},
"save": "Save"
}
},
"domain": {
"count_header": "Domain(s)"
},
"error": {
"404": "404 - Not found",
"create_failed": "Create failed",
"delete_failed": "Delete failed",
"missing_permissions": "Missing permissions: {{permissions}}",
"network_error": "Network Error",
"permission_denied": "Permission Denied",
"restore_failed": "Restore failed",
"unauthorized": "Unauthorized",
"update_failed": "Update failed"
"retry": "Retry",
"server_unavailable": "Server unavailable",
"unauthorized": "Unauthorized"
},
"footer": {
"imprint": "Imprint",
@ -84,11 +75,12 @@
"count_header": "Group(s)"
},
"header": {
"logout": "Logout",
"menu": {
"about": "About",
"admin": "Admin"
}
"logout": "Logout"
},
"permission_descriptions": {
"short_urls": "See all URLs",
"short_urls.by_assignment": "See all short urls assigned to a group by role",
"users.update": "Change users including their roles"
},
"permissions": {
"api_keys": "API Keys",
@ -107,10 +99,6 @@
"ip_list.create": "Create",
"ip_list.delete": "Delete",
"ip_list.update": "Update",
"news": "News",
"news.create": "Create",
"news.delete": "Delete",
"news.update": "Update",
"roles": "Roles",
"roles.create": "Create",
"roles.delete": "Delete",
@ -125,19 +113,10 @@
"users.delete": "Delete",
"users.update": "Update"
},
"permission_descriptions": {
"users.update": "Change users including their roles",
"short_urls": "See all URLs",
"short_urls.by_assignment": "See all short urls assigned to a group by role"
},
"qr": {
"width": "Width"
},
"role": {
"count_header": "Role(s)"
},
"short_url": {
"count_header": "Url(s)",
"loading_screen": "Loading screen",
"short_url": "URL",
"target_url": "Target",
@ -146,12 +125,13 @@
"sidebar": {
"administration": "Administration",
"api_keys": "API Keys",
"feature_flags": "Features",
"header": {
"create": "create",
"update": "update"
},
"public": "Public",
"roles": "Roles",
"settings": "Settings",
"users": "Users"
},
"table": {
@ -163,6 +143,7 @@
"reset_filters": "Reset filters",
"reset_sort": "Reset sort",
"restore": "Restore",
"select_columns": "Select columns",
"show_deleted": "Show deleted",
"to": "to",
"update": "Update"