diff --git a/Makefile b/Makefile index a6ea880..2dc391c 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ +tag = "$$(git describe --tags)" + build: $(MAKE) -C backend build - docker push yksflip/dashboard-backend:latest docker build -t dashboard . - docker tag dashboard yksflip/dashboard:latest - docker push yksflip/dashboard:latest + docker tag dashboard yksflip/dashboard:$(tag) + docker push yksflip/dashboard:$(tag) rm: docker stack rm ${STACK_NAME} diff --git a/backend/.env.sample b/backend/.env.sample index 876837d..7b053cb 100644 --- a/backend/.env.sample +++ b/backend/.env.sample @@ -7,3 +7,4 @@ REDIRECT_URL="https://example.org/login-callback" SECRET_KEY= LOAD_INCLUSTER_CONFIG=false DATABASE_URL=sqlite:///database.db +AUTHENTIK_BASEURL="https://sso.example.org/api/v3" \ No newline at end of file diff --git a/backend/Makefile b/backend/Makefile index 7b85e16..9ed036b 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,6 +1,9 @@ +tag = "$$(git describe --tags)" + build: docker build -t dashboard-backend . - docker tag dashboard-backend yksflip/dashboard-backend:latest + docker tag dashboard-backend yksflip/dashboard-backend:$(tag) + docker push yksflip/dashboard-backend:$(tag) clean: rm database.db diff --git a/backend/areas/apps/models_lit.py b/backend/areas/apps/models_lit.py deleted file mode 100644 index 1132970..0000000 --- a/backend/areas/apps/models_lit.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Everything to do with Apps""" - -from database import db -from .models import App - -class LITApp(App): - """ - """ - - def get_url(self): - return self.url - - def to_dict(self): - """ - represent this object as a dict, compatible for JSON output - """ - return {"id": self.id, - "name": self.name, - "slug": self.slug, - "external": self.external, - "status": self.get_status(), - "url": self.get_url()} - - - - def get_status(self): - """Returns an AppStatus object that describes the current cluster state""" - return { - "installed": "", - "ready": "", - "message": "", - } diff --git a/backend/areas/auth/auth.py b/backend/areas/auth/auth.py index 4e96088..c972752 100644 --- a/backend/areas/auth/auth.py +++ b/backend/areas/auth/auth.py @@ -30,39 +30,38 @@ def hydra_callback(): token = HydraOauth.get_token(state, code) user_info = HydraOauth.get_user_info() # Match Kratos identity with Hydra - # identities = KratosApi.get("/identities") - # identity = None - # for i in identities.json(): - # if i["traits"]["email"] == user_info["email"]: - # identity = i + identities = KratosApi.get("/identities") + identity = None + for i in identities.json(): + if i["traits"]["email"] == user_info["email"]: + identity = i access_token = create_access_token( - identity=token, expires_delta=timedelta(days=365), - #additional_claims={"user_id": identity["id"]} + identity=token, expires_delta=timedelta(days=365), additional_claims={"user_id": identity["id"]} ) - # apps = App.query.all() - # app_roles = [] - # for app in apps: - # tmp_app_role = AppRole.query.filter_by( - # user_id=identity["id"], app_id=app.id - # ).first() - # app_roles.append( - # { - # "name": app.slug, - # "role_id": tmp_app_role.role_id if tmp_app_role else None, - # } - # ) + apps = App.query.all() + app_roles = [] + for app in apps: + tmp_app_role = AppRole.query.filter_by( + user_id=identity["id"], app_id=app.id + ).first() + app_roles.append( + { + "name": app.slug, + "role_id": tmp_app_role.role_id if tmp_app_role else None, + } + ) return jsonify( { "accessToken": access_token, "userInfo": { - "id": user_info["email"], + "id": identity["id"], "email": user_info["email"], "name": user_info["name"], "preferredUsername": user_info["preferred_username"], - # "app_roles": app_roles, + "app_roles": app_roles, }, } ) diff --git a/backend/areas/auth/lit_auth.py b/backend/areas/auth/lit_auth.py index f804690..f99432c 100644 --- a/backend/areas/auth/lit_auth.py +++ b/backend/areas/auth/lit_auth.py @@ -5,15 +5,14 @@ from flask_cors import cross_origin from datetime import timedelta from areas import api_v1 -from areas.apps import App, AppRole from config import * -from helpers import HydraOauth, BadRequest +from helpers import LITOauth, BadRequest @api_v1.route("/login", methods=["POST"]) @cross_origin() def login(): - authorization_url = HydraOauth.authorize() + authorization_url = LITOauth.authorize() return jsonify({"authorizationUrl": authorization_url}) @@ -28,26 +27,17 @@ def hydra_callback(): if code == None: raise BadRequest("Missing code query param") - token = HydraOauth.get_token(state, code) - user_info = HydraOauth.get_user_info() - + token = LITOauth.get_token(state, code) + user_info = LITOauth.get_user_info() access_token = create_access_token( - identity=token, expires_delta=timedelta(days=365), - #additional_claims={"user_id": identity["id"]} - ) - - # apps = App.query.all() - # app_roles = [] - # for app in apps: - # tmp_app_role = AppRole.query.filter_by( - # user_id=identity["id"], app_id=app.id - # ).first() - # app_roles.append( - # { - # "name": app.slug, - # "role_id": tmp_app_role.role_id if tmp_app_role else None, - # } - # ) + identity=token, expires_delta=timedelta(days=365)) + isAdmin = "admin" in user_info["groups"] + app_roles = [ + { + "name": "dashboard", + "role_id": 1 if isAdmin else 2 + }, + ] return jsonify( { @@ -57,7 +47,7 @@ def hydra_callback(): "email": user_info["email"], "name": user_info["name"], "preferredUsername": user_info["preferred_username"], - # "app_roles": app_roles, - }, + "app_roles": app_roles + } } ) diff --git a/backend/areas/users/lit_user_service.py b/backend/areas/users/lit_user_service.py new file mode 100644 index 0000000..c6b1581 --- /dev/null +++ b/backend/areas/users/lit_user_service.py @@ -0,0 +1,39 @@ +from flask import current_app +from helpers.authentik_api import AuthentikApi +from .user_service import UserService +from .models import User + + +class UserService(UserService): + @staticmethod + def get_users(): + user_list = [User.from_authentik(u).to_dict() for u in AuthentikApi.get("/core/users")] + return user_list + + @staticmethod + def get_user(id): + pass + + @staticmethod + def post_user(data): + pass + + @staticmethod + def __start_recovery_flow(email): + pass + + @staticmethod + def put_user(id, user_editing_id, data): + pass + + @staticmethod + def delete_user(id): + pass + + @staticmethod + def post_multiple_users(data): + pass + + @staticmethod + def __insertAppRoleToUser(userId, userRes): + pass diff --git a/backend/areas/users/models.py b/backend/areas/users/models.py new file mode 100644 index 0000000..70ace01 --- /dev/null +++ b/backend/areas/users/models.py @@ -0,0 +1,37 @@ + + +class User: + id = None + uuid = None + traits = None + email = None + name = None + preferredUsername = None + state = None + + def __init__(self): + pass + + @staticmethod + def from_authentik(authentik_user): + u = User() + u.id = authentik_user["pk"] + u.uuid = authentik_user["uid"] + u.name = authentik_user["name"] + u.email = authentik_user["email"] + u.traits = { + "name": authentik_user["name"], + "email": authentik_user["email"], + "app_roles": [] + } + u.preferredUsername = authentik_user["username"] + u.state = "active" if authentik_user["is_active"] else "" + return u + + def to_dict(self): + return { + "id": self.id, + "traits": self.traits, + "preferredUsername": self.preferredUsername, + "state": self.state, + } diff --git a/backend/areas/users/users.py b/backend/areas/users/users.py index 08f22c6..290dcb3 100644 --- a/backend/areas/users/users.py +++ b/backend/areas/users/users.py @@ -8,16 +8,15 @@ from helpers import KratosApi from helpers.auth_guard import admin_required from .validation import schema, schema_multiple -from .user_service import UserService +from .lit_user_service import UserService @api_v1.route("/users", methods=["GET"]) @jwt_required() @cross_origin() -@admin_required() +# @admin_required() TODO: not needed as authentik checks permissions? def get_users(): - res = UserService.get_users() - return jsonify(res) + return jsonify(UserService.get_users()) @api_v1.route("/users/", methods=["GET"]) diff --git a/backend/config.py b/backend/config.py index 15874a9..4daddfa 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,11 +1,13 @@ import os + def env_file(key: str): file_env = os.environ.get(f"{key}_FILE") if file_env and os.path.exists(file_env): return open(file_env).read().rstrip('\n') return os.environ.get(key) + SECRET_KEY = env_file("SECRET_KEY") HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID") @@ -28,4 +30,7 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False # Set this to "true" to load the config from a Kubernetes serviceaccount # running in a Kubernetes pod. Set it to "false" to load the config from the # `KUBECONFIG` environment variable. -LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG").lower() == "true" +LOAD_INCLUSTER_CONFIG = os.environ.get( + "LOAD_INCLUSTER_CONFIG").lower() == "true" + +AUTHENTIK_BASEURL = os.environ.get("AUTHENTIK_BASEURL") diff --git a/backend/helpers/__init__.py b/backend/helpers/__init__.py index 9302e8e..bc10551 100644 --- a/backend/helpers/__init__.py +++ b/backend/helpers/__init__.py @@ -2,3 +2,4 @@ from .kratos_api import * from .error_handler import * from .hydra_oauth import * from .kratos_user import * +from .lit_oauth import * \ No newline at end of file diff --git a/backend/helpers/auth_guard.py b/backend/helpers/auth_guard.py index 36bbeeb..90ea5fc 100644 --- a/backend/helpers/auth_guard.py +++ b/backend/helpers/auth_guard.py @@ -12,6 +12,7 @@ def admin_required(): def decorator(*args, **kwargs): verify_jwt_in_request() claims = get_jwt() + user_id = claims["user_id"] is_admin = RoleService.is_user_admin(user_id) if is_admin: diff --git a/backend/helpers/authentik_api.py b/backend/helpers/authentik_api.py new file mode 100644 index 0000000..e8e9a4d --- /dev/null +++ b/backend/helpers/authentik_api.py @@ -0,0 +1,42 @@ +from typing import List +from flask_jwt_extended import get_jwt +import requests + +from config import AUTHENTIK_BASEURL +from .error_handler import AuthentikError + +class AuthentikApi: # TODO: check if can be replaced with apispec generated api? + @staticmethod + def __handleError(res): + if res.status_code >= 400: + message = res.json()["error"]["message"] + raise AuthentikError(message, res.status_code) + + @staticmethod + def __token(): + jwt = get_jwt() + return jwt["sub"]["refresh_token"] + + @staticmethod + def get(url): + try: + res = requests.get(f"{AUTHENTIK_BASEURL}{url}", headers={ + "Authorization": f"Bearer {AuthentikApi.__token()}"}) + AuthentikApi.__handleError(res) + if ("pagination" in res.json()): + return AuthentikApi.__paginate(res) + return res.json() + except AuthentikError as err: + raise err + except: + raise AuthentikError() + + @staticmethod + def __paginate(res: requests.Response): # TODO: test this + results = res.json()["results"] + for page in range(1, res.json()["pagination"]["total_pages"]): + res = requests.get( + f"{res.request.url}", headers=res.request.headers, params={'page': page}) + AuthentikApi.__handleError(res) + results.append(res.json()["results"]) + return results \ No newline at end of file diff --git a/backend/helpers/error_handler.py b/backend/helpers/error_handler.py index d4009b8..b1edcb7 100644 --- a/backend/helpers/error_handler.py +++ b/backend/helpers/error_handler.py @@ -5,6 +5,8 @@ from jsonschema import ValidationError class KratosError(Exception): pass +class AuthentikError(Exception): + pass class HydraError(Exception): pass diff --git a/backend/helpers/hydra_oauth.py b/backend/helpers/hydra_oauth.py index 5143487..b75d615 100644 --- a/backend/helpers/hydra_oauth.py +++ b/backend/helpers/hydra_oauth.py @@ -9,7 +9,7 @@ class HydraOauth: @staticmethod def authorize(): try: - hydra = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL) + hydra = OAuth2Session(HYDRA_CLIENT_ID) authorization_url, state = hydra.authorization_url( HYDRA_AUTHORIZATION_BASE_URL ) diff --git a/backend/helpers/kratos_api.py b/backend/helpers/kratos_api.py index eb83c82..af9f6e0 100644 --- a/backend/helpers/kratos_api.py +++ b/backend/helpers/kratos_api.py @@ -4,7 +4,6 @@ import requests from config import * from .error_handler import KratosError - class KratosApi: @staticmethod def __handleError(res): diff --git a/backend/helpers/lit_oauth.py b/backend/helpers/lit_oauth.py new file mode 100644 index 0000000..3df84d2 --- /dev/null +++ b/backend/helpers/lit_oauth.py @@ -0,0 +1,51 @@ +from flask import request, session +from requests_oauthlib import OAuth2Session + +from config import * +from helpers import HydraError + + +class LITOauth: + @staticmethod + def authorize(): + try: + scopes = ["openid", "email", "profile", "goauthentik.io/api"] + oauth = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL, scope=scopes) + authorization_url, state = oauth.authorization_url( + HYDRA_AUTHORIZATION_BASE_URL + ) + return authorization_url + except Exception as err: + raise HydraError(str(err), 500) + + @staticmethod + def get_token(state, code): + try: + oauth = OAuth2Session( + client_id=HYDRA_CLIENT_ID, + state=state, + ) + token = oauth.fetch_token( + token_url=TOKEN_URL, + code=code, + client_secret=HYDRA_CLIENT_SECRET, + include_client_id=True, + ) + + session["oauth_token"] = token + + return token + except Exception as err: + raise HydraError(str(err), 500) + + @staticmethod + def get_user_info(): + try: + hydra = OAuth2Session( + client_id=HYDRA_CLIENT_ID, token=session["oauth_token"] + ) + user_info = hydra.get("{}/userinfo".format(HYDRA_PUBLIC_URL)) + + return user_info.json() + except Exception as err: + raise HydraError(str(err), 500) diff --git a/backend/requirements.txt b/backend/requirements.txt index eae5bd2..a1e705d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,8 @@ +alembic==1.8.1 attrs==21.4.0 black==22.1.0 +cachelib==0.9.0 +cachetools==5.2.0 certifi==2021.10.8 cffi==1.15.0 charset-normalizer==2.0.12 @@ -9,32 +12,43 @@ Flask==2.0.3 Flask-Cors==3.0.10 flask-expects-json==1.7.0 Flask-JWT-Extended==4.3.1 +Flask-Migrate==3.1.0 +Flask-Session==0.4.0 +Flask-SQLAlchemy==2.5.1 +google-auth==2.14.0 +greenlet==2.0.0.post0 gunicorn==20.1.0 +hydra-client==0.4.0 idna==3.3 install==1.3.5 itsdangerous==2.1.1 -jsonschema==4.4.0 Jinja2==3.0.3 jinja2-base64-filters==0.1.4 +jsonschema==4.4.0 kubernetes==24.2.0 +Mako==1.2.3 MarkupSafe==2.1.1 mypy-extensions==0.4.3 oauthlib==3.2.0 +ory-kratos-client==0.9.0a2 pathspec==0.9.0 platformdirs==2.5.1 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 pycparser==2.21 PyJWT==2.3.0 +PyMySQL==1.0.2 pyrsistent==0.18.1 +python-dateutil==2.8.2 +PyYAML==6.0 regex==2022.3.15 requests==2.27.1 requests-oauthlib==1.3.1 +rsa==4.9 six==1.16.0 +SQLAlchemy==1.4.42 tomli==1.2.3 -typing-extensions==4.1.1 +typing_extensions==4.1.1 urllib3==1.26.8 +websocket-client==1.4.2 Werkzeug==2.0.3 -ory-kratos-client==0.9.0a2 -pymysql -Flask-SQLAlchemy -hydra-client -Flask-Migrate diff --git a/backend/web/static/base.js b/backend/web/static/base.js index 0e142ed..2ca8456 100644 --- a/backend/web/static/base.js +++ b/backend/web/static/base.js @@ -1,6 +1,6 @@ /* base.js This is the base JS file to render the user interfaces of kratos and provide - the end user with flows for login, recovery etc. + the end user with flows for login, recovery etc. check_flow_*(): These functions check the status of the flow and based on the status do some @@ -261,8 +261,6 @@ function render_messages(data) { // value: If there is already a value known, show it // messages: error messages related to the field function getFormElement(type, name, value, messages) { - console.log('Getting form element', type, name, value, messages); - if (value == undefined) { value = ''; } @@ -350,7 +348,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) { if (typeof help == 'undefined' || help == null) { help = ''; } - console.log('Messages: ', messages); // Id field for help element var nameHelp = name + 'Help'; @@ -362,7 +359,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) { // messages get appended to help info if (messages.length) { for (message in messages) { - console.log('adding message', messages[message]); help += messages[message]['text']; } } diff --git a/public/assets/authentik.svg b/public/assets/authentik.svg new file mode 100644 index 0000000..c839dda --- /dev/null +++ b/public/assets/authentik.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/user.svg b/public/assets/user.svg deleted file mode 100644 index 9f069e0..0000000 --- a/public/assets/user.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - Svg Vector Icons : http://www.onlinewebfonts.com/icon - - \ No newline at end of file diff --git a/public/assets/zammad.svg b/public/assets/zammad.svg new file mode 100644 index 0000000..0685eb7 --- /dev/null +++ b/public/assets/zammad.svg @@ -0,0 +1,30 @@ + + + + logo + Created with Sketch. + + + + + \ No newline at end of file diff --git a/public/env.js b/public/env.js index d47bfbb..1421897 100644 --- a/public/env.js +++ b/public/env.js @@ -1,3 +1,4 @@ window.env = { REACT_APP_API_URL: 'http://localhost:5000/api/v1', + REACT_APP_SSO_LOGOUT_URL: 'https://login.example.org/if/flow/default-invalidation-flow/' }; diff --git a/public/markdown/authentik.md b/public/markdown/authentik.md new file mode 100644 index 0000000..3c48a26 --- /dev/null +++ b/public/markdown/authentik.md @@ -0,0 +1 @@ +Verwalte User und Gruppen \ No newline at end of file diff --git a/public/markdown/zammad.md b/public/markdown/zammad.md new file mode 100644 index 0000000..b1252f6 --- /dev/null +++ b/public/markdown/zammad.md @@ -0,0 +1 @@ +Ticketsystem \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 83bb6f2..e225b1b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,6 @@ import { Login } from './modules/login'; import { Users } from './modules/users/Users'; import { AppSingle } from './modules/apps/AppSingle'; import { Apps } from './modules/apps/Apps'; -import { DashboardLIT } from './modules/dashboard/DashboardLIT'; // eslint-disable-next-line @typescript-eslint/no-unused-vars function App() { @@ -53,7 +52,7 @@ function App() { ) : ( - } /> + } /> {apps.map((app) => ( } /> ))} diff --git a/src/components/Header/HeaderLIT.tsx b/src/components/Header/HeaderLIT.tsx index d385158..7a4cc75 100644 --- a/src/components/Header/HeaderLIT.tsx +++ b/src/components/Header/HeaderLIT.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useMemo, useState } from 'react'; +import React, { Fragment, useMemo } from 'react'; import { Disclosure, Menu, Transition } from '@headlessui/react'; import { MenuIcon, XIcon } from '@heroicons/react/outline'; import { useAuth } from 'src/services/auth'; @@ -6,16 +6,10 @@ import Gravatar from 'react-gravatar'; import { Link, useLocation } from 'react-router-dom'; import clsx from 'clsx'; import { useApps } from 'src/services/apps'; -import _ from 'lodash'; - -import { UserModal } from '../UserModal'; - -const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`; const navigation = [ - { name: 'Dashboard', to: '/dashboard', requiresAdmin: false }, - { name: 'Users', to: '/users', requiresAdmin: true }, - { name: 'Apps', to: '/apps', requiresAdmin: true }, + { name: '', to: '/users', requiresAdmin: true }, + // { name: 'Apps', to: '/apps', requiresAdmin: true }, ]; function classNames(...classes: any[]) { @@ -34,32 +28,15 @@ function filterNavigationByDashboardRole(isAdmin: boolean) { interface HeaderProps {} const HeaderLIT: React.FC = () => { - const [currentUserModal, setCurrentUserModal] = useState(false); - const [currentUserId, setCurrentUserId] = useState(null); const { logOut, currentUser, isAdmin } = useAuth(); const { pathname } = useLocation(); - const { apps, loadApps, appTableLoading } = useApps(); - - const currentUserModalOpen = (id: any) => { - setCurrentUserId(id); - setCurrentUserModal(true); - }; - - const currentUserModalClose = () => { - setCurrentUserModal(false); - setCurrentUserId(null); - }; - + const { apps } = useApps(); const navigationItems = filterNavigationByDashboardRole(isAdmin); const signOutUrl = useMemo(() => { - const { hostname } = window.location; - // If we are developing locally, we need to use the init cluster's public URL - if (hostname === 'localhost') { - return HYDRA_LOGOUT_URL; - } - return `https://${hostname.replace(/^dashboard/, 'sso')}/oauth2/sessions/logout`; + // @ts-ignore + return window.env.REACT_APP_SSO_LOGOUT_URL; }, []); return ( @@ -102,6 +79,21 @@ const HeaderLIT: React.FC = () => { {app.name} ))} + {/* {navigationItems.map((item) => ( + + {item.name} + + ))} */}
@@ -167,10 +159,6 @@ const HeaderLIT: React.FC = () => {
)} - - {currentUserModal && ( - - )} ); }; diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts index 527f03a..8295dc8 100644 --- a/src/components/Header/index.ts +++ b/src/components/Header/index.ts @@ -1,2 +1 @@ -export { default as Header } from './Header'; -export { default as HeaderLIT } from './HeaderLIT'; +export { default as Header } from './HeaderLIT'; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 9467e21..ab60d93 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { HeaderLIT } from '../Header'; +import { Header } from '../Header'; const Layout: React.FC = ({ children }) => { return ( <> - +
{children} diff --git a/src/components/index.ts b/src/components/index.ts index 7c60c55..6f9b27a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,5 +1,5 @@ export { Layout } from './Layout'; -export { Header, HeaderLIT } from './Header'; +export { Header } from './Header'; export { Table } from './Table'; export { Banner } from './Banner'; export { Tabs } from './Tabs'; diff --git a/src/modules/dashboard/DashboardLIT.tsx b/src/modules/dashboard/DashboardLIT.tsx index daab975..47a3305 100644 --- a/src/modules/dashboard/DashboardLIT.tsx +++ b/src/modules/dashboard/DashboardLIT.tsx @@ -1,9 +1,13 @@ import React, { useEffect } from 'react'; -import { useApps } from 'src/services/apps'; -import { DashboardCardLIT } from './components/DashboardCard/DashboardCardLIT'; +import { AppStatusEnum, useApps } from 'src/services/apps'; +import { useAuth } from 'src/services/auth'; +import { DashboardUtility } from './components'; +import { DashboardCard } from './components/DashboardCard/DashboardCardLIT'; +import { UTILITY_APPS } from './consts'; -export const DashboardLIT: React.FC = () => { +export const Dashboard: React.FC = () => { const { apps, loadApps, appTableLoading } = useApps(); + const { isAdmin } = useAuth(); // Tell React to load the apps useEffect(() => { @@ -21,9 +25,27 @@ export const DashboardLIT: React.FC = () => {
- {!appTableLoading && apps.map((app) => )} + {!appTableLoading && + apps + .filter((app) => UTILITY_APPS.indexOf(app.slug) === -1) + .map((app) => )}
+ {isAdmin && ( +
+
+

Administration

+
+ +
+ {apps + .filter((app) => UTILITY_APPS.indexOf(app.slug) !== -1 && app.url !== null) + .map((app) => ( + + ))} +
+
+ )} ); }; diff --git a/src/modules/dashboard/components/DashboardCard/DashboardCardLIT.tsx b/src/modules/dashboard/components/DashboardCard/DashboardCardLIT.tsx index 735f833..f82587e 100644 --- a/src/modules/dashboard/components/DashboardCard/DashboardCardLIT.tsx +++ b/src/modules/dashboard/components/DashboardCard/DashboardCardLIT.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; -export const DashboardCardLIT: React.FC = ({ app }: { app: any }) => { +export const DashboardCard: React.FC = ({ app }: { app: any }) => { return ( <> = ({ app }: { app: any }) => { className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0" key={app.name} > -
+
= ({ item }: { item: any }) => { + const [content, setContent] = useState(''); + + useEffect(() => { + fetch(item.markdownSrc) + .then((res) => res.text()) + .then((md) => { + return setContent(md); + }) + .catch(() => {}); + }, [item.markdownSrc]); + + return ( + +
+ {item.icon &&
+
+
{item.name}
+
+ {content} +
+
+ + ); +}; diff --git a/src/modules/dashboard/components/DashboardUtility/index.ts b/src/modules/dashboard/components/DashboardUtility/index.ts index 24e4b68..1e5aba6 100644 --- a/src/modules/dashboard/components/DashboardUtility/index.ts +++ b/src/modules/dashboard/components/DashboardUtility/index.ts @@ -1 +1 @@ -export { DashboardUtility } from './DashboardUtility'; +export { DashboardUtility } from './DashboardUtilityLIT'; diff --git a/src/modules/dashboard/consts.ts b/src/modules/dashboard/consts.ts index d31a0a9..10bce99 100644 --- a/src/modules/dashboard/consts.ts +++ b/src/modules/dashboard/consts.ts @@ -10,7 +10,7 @@ export const DASHBOARD_QUICK_ACCESS = [ ]; /** Apps that should not be shown on the dashboard */ -export const HIDDEN_APPS = ['dashboard', 'velero']; +export const HIDDEN_APPS = ['dashboard']; /** Apps that should be shown under "Utilities" */ -export const UTILITY_APPS = ['monitoring']; +export const UTILITY_APPS = ['authentik', 'zammad']; diff --git a/src/modules/dashboard/index.ts b/src/modules/dashboard/index.ts index 41a54b5..f090df8 100644 --- a/src/modules/dashboard/index.ts +++ b/src/modules/dashboard/index.ts @@ -1 +1 @@ -export { Dashboard } from './Dashboard'; +export { Dashboard } from './DashboardLIT'; diff --git a/src/modules/users/Users.tsx b/src/modules/users/Users.tsx index 5d45dd5..df0321d 100644 --- a/src/modules/users/Users.tsx +++ b/src/modules/users/Users.tsx @@ -53,6 +53,11 @@ export const Users: React.FC = () => { const columns: any = React.useMemo( () => [ + { + Header: 'Username', + accessor: 'preferredUsername', + width: 'auto', + }, { Header: 'Name', accessor: 'name', @@ -75,11 +80,12 @@ export const Users: React.FC = () => { if (isAdmin) { return ( -
+