introduce admin area

introduce admin area
first poc for connecting the authentik api

Co-authored-by: Philipp Rothmann <philipprothmann@posteo.de>
Reviewed-on: #2
single-logout v0.1.3
philipp 2022-11-08 16:36:16 +01:00
parent 8d760e588f
commit 44e4e4eb42
35 changed files with 367 additions and 133 deletions

View File

@ -1,9 +1,10 @@
tag = "$$(git describe --tags)"
build: build:
$(MAKE) -C backend build $(MAKE) -C backend build
docker push yksflip/dashboard-backend:latest
docker build -t dashboard . docker build -t dashboard .
docker tag dashboard yksflip/dashboard:latest docker tag dashboard yksflip/dashboard:$(tag)
docker push yksflip/dashboard:latest docker push yksflip/dashboard:$(tag)
rm: rm:
docker stack rm ${STACK_NAME} docker stack rm ${STACK_NAME}

View File

@ -7,3 +7,4 @@ REDIRECT_URL="https://example.org/login-callback"
SECRET_KEY= SECRET_KEY=
LOAD_INCLUSTER_CONFIG=false LOAD_INCLUSTER_CONFIG=false
DATABASE_URL=sqlite:///database.db DATABASE_URL=sqlite:///database.db
AUTHENTIK_BASEURL="https://sso.example.org/api/v3"

View File

@ -1,6 +1,9 @@
tag = "$$(git describe --tags)"
build: build:
docker build -t dashboard-backend . 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: clean:
rm database.db rm database.db

View File

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

View File

@ -5,15 +5,14 @@ from flask_cors import cross_origin
from datetime import timedelta from datetime import timedelta
from areas import api_v1 from areas import api_v1
from areas.apps import App, AppRole
from config import * from config import *
from helpers import HydraOauth, BadRequest from helpers import LITOauth, BadRequest
@api_v1.route("/login", methods=["POST"]) @api_v1.route("/login", methods=["POST"])
@cross_origin() @cross_origin()
def login(): def login():
authorization_url = HydraOauth.authorize() authorization_url = LITOauth.authorize()
return jsonify({"authorizationUrl": authorization_url}) return jsonify({"authorizationUrl": authorization_url})
@ -28,26 +27,17 @@ def hydra_callback():
if code == None: if code == None:
raise BadRequest("Missing code query param") raise BadRequest("Missing code query param")
token = HydraOauth.get_token(state, code) token = LITOauth.get_token(state, code)
user_info = HydraOauth.get_user_info() user_info = LITOauth.get_user_info()
access_token = create_access_token( access_token = create_access_token(
identity=token, expires_delta=timedelta(days=365), identity=token, expires_delta=timedelta(days=365))
#additional_claims={"user_id": identity["id"]} isAdmin = "admin" in user_info["groups"]
) app_roles = [
{
# apps = App.query.all() "name": "dashboard",
# app_roles = [] "role_id": 1 if isAdmin else 2
# 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( return jsonify(
{ {
@ -57,7 +47,7 @@ def hydra_callback():
"email": user_info["email"], "email": user_info["email"],
"name": user_info["name"], "name": user_info["name"],
"preferredUsername": user_info["preferred_username"], "preferredUsername": user_info["preferred_username"],
# "app_roles": app_roles, "app_roles": app_roles
}, }
} }
) )

View File

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

View File

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

View File

@ -8,16 +8,15 @@ from helpers import KratosApi
from helpers.auth_guard import admin_required from helpers.auth_guard import admin_required
from .validation import schema, schema_multiple from .validation import schema, schema_multiple
from .user_service import UserService from .lit_user_service import UserService
@api_v1.route("/users", methods=["GET"]) @api_v1.route("/users", methods=["GET"])
@jwt_required() @jwt_required()
@cross_origin() @cross_origin()
@admin_required() # @admin_required() TODO: not needed as authentik checks permissions?
def get_users(): def get_users():
res = UserService.get_users() return jsonify(UserService.get_users())
return jsonify(res)
@api_v1.route("/users/<string:id>", methods=["GET"]) @api_v1.route("/users/<string:id>", methods=["GET"])

View File

@ -1,11 +1,13 @@
import os import os
def env_file(key: str): def env_file(key: str):
file_env = os.environ.get(f"{key}_FILE") file_env = os.environ.get(f"{key}_FILE")
if file_env and os.path.exists(file_env): if file_env and os.path.exists(file_env):
return open(file_env).read().rstrip('\n') return open(file_env).read().rstrip('\n')
return os.environ.get(key) return os.environ.get(key)
SECRET_KEY = env_file("SECRET_KEY") SECRET_KEY = env_file("SECRET_KEY")
HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID") 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 # 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 # running in a Kubernetes pod. Set it to "false" to load the config from the
# `KUBECONFIG` environment variable. # `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")

View File

@ -2,3 +2,4 @@ from .kratos_api import *
from .error_handler import * from .error_handler import *
from .hydra_oauth import * from .hydra_oauth import *
from .kratos_user import * from .kratos_user import *
from .lit_oauth import *

View File

@ -12,6 +12,7 @@ def admin_required():
def decorator(*args, **kwargs): def decorator(*args, **kwargs):
verify_jwt_in_request() verify_jwt_in_request()
claims = get_jwt() claims = get_jwt()
user_id = claims["user_id"] user_id = claims["user_id"]
is_admin = RoleService.is_user_admin(user_id) is_admin = RoleService.is_user_admin(user_id)
if is_admin: if is_admin:

View File

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

View File

@ -5,6 +5,8 @@ from jsonschema import ValidationError
class KratosError(Exception): class KratosError(Exception):
pass pass
class AuthentikError(Exception):
pass
class HydraError(Exception): class HydraError(Exception):
pass pass

View File

@ -9,7 +9,7 @@ class HydraOauth:
@staticmethod @staticmethod
def authorize(): def authorize():
try: try:
hydra = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL) hydra = OAuth2Session(HYDRA_CLIENT_ID)
authorization_url, state = hydra.authorization_url( authorization_url, state = hydra.authorization_url(
HYDRA_AUTHORIZATION_BASE_URL HYDRA_AUTHORIZATION_BASE_URL
) )

View File

@ -4,7 +4,6 @@ import requests
from config import * from config import *
from .error_handler import KratosError from .error_handler import KratosError
class KratosApi: class KratosApi:
@staticmethod @staticmethod
def __handleError(res): def __handleError(res):

View File

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

View File

@ -1,5 +1,8 @@
alembic==1.8.1
attrs==21.4.0 attrs==21.4.0
black==22.1.0 black==22.1.0
cachelib==0.9.0
cachetools==5.2.0
certifi==2021.10.8 certifi==2021.10.8
cffi==1.15.0 cffi==1.15.0
charset-normalizer==2.0.12 charset-normalizer==2.0.12
@ -9,32 +12,43 @@ Flask==2.0.3
Flask-Cors==3.0.10 Flask-Cors==3.0.10
flask-expects-json==1.7.0 flask-expects-json==1.7.0
Flask-JWT-Extended==4.3.1 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 gunicorn==20.1.0
hydra-client==0.4.0
idna==3.3 idna==3.3
install==1.3.5 install==1.3.5
itsdangerous==2.1.1 itsdangerous==2.1.1
jsonschema==4.4.0
Jinja2==3.0.3 Jinja2==3.0.3
jinja2-base64-filters==0.1.4 jinja2-base64-filters==0.1.4
jsonschema==4.4.0
kubernetes==24.2.0 kubernetes==24.2.0
Mako==1.2.3
MarkupSafe==2.1.1 MarkupSafe==2.1.1
mypy-extensions==0.4.3 mypy-extensions==0.4.3
oauthlib==3.2.0 oauthlib==3.2.0
ory-kratos-client==0.9.0a2
pathspec==0.9.0 pathspec==0.9.0
platformdirs==2.5.1 platformdirs==2.5.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.21 pycparser==2.21
PyJWT==2.3.0 PyJWT==2.3.0
PyMySQL==1.0.2
pyrsistent==0.18.1 pyrsistent==0.18.1
python-dateutil==2.8.2
PyYAML==6.0
regex==2022.3.15 regex==2022.3.15
requests==2.27.1 requests==2.27.1
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
rsa==4.9
six==1.16.0 six==1.16.0
SQLAlchemy==1.4.42
tomli==1.2.3 tomli==1.2.3
typing-extensions==4.1.1 typing_extensions==4.1.1
urllib3==1.26.8 urllib3==1.26.8
websocket-client==1.4.2
Werkzeug==2.0.3 Werkzeug==2.0.3
ory-kratos-client==0.9.0a2
pymysql
Flask-SQLAlchemy
hydra-client
Flask-Migrate

View File

@ -1,6 +1,6 @@
/* base.js /* base.js
This is the base JS file to render the user interfaces of kratos and provide 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_*(): check_flow_*():
These functions check the status of the flow and based on the status do some 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 // value: If there is already a value known, show it
// messages: error messages related to the field // messages: error messages related to the field
function getFormElement(type, name, value, messages) { function getFormElement(type, name, value, messages) {
console.log('Getting form element', type, name, value, messages);
if (value == undefined) { if (value == undefined) {
value = ''; value = '';
} }
@ -350,7 +348,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
if (typeof help == 'undefined' || help == null) { if (typeof help == 'undefined' || help == null) {
help = ''; help = '';
} }
console.log('Messages: ', messages);
// Id field for help element // Id field for help element
var nameHelp = name + 'Help'; var nameHelp = name + 'Help';
@ -362,7 +359,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
// messages get appended to help info // messages get appended to help info
if (messages.length) { if (messages.length) {
for (message in messages) { for (message in messages) {
console.log('adding message', messages[message]);
help += messages[message]['text']; help += messages[message]['text'];
} }
} }

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="126px" height="108px" viewBox="0 0 42 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>logo</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="logo" sketch:type="MSArtboardGroup">
<g sketch:type="MSLayerGroup" transform="translate(1.000000, 0.000000)" id="Shape">
<path d="M27.3375,12.6 L36.72,9.72 L31.1625,13.2525 L27.3375,12.6 Z" fill="#CA2317" sketch:type="MSShapeGroup"></path>
<path d="M33.0525,19.62 L31.1625,13.2525 L36.72,9.72 L35.055,15.435 L33.0525,19.62 Z" fill="#E84F83" sketch:type="MSShapeGroup"></path>
<path d="M39.465,7.9875 L38.43,9.72 L35.055,15.435 L36.72,9.72 L39.465,7.9875 Z" fill="#CA2317" sketch:type="MSShapeGroup"></path>
<path d="M39.8025,9.1125 L37.1925,11.79 L38.43,9.72 L39.8025,9.1125 Z" fill="#E54011" sketch:type="MSShapeGroup"></path>
<path d="M27.9,10.8225 L35.5725,10.0575 L30.24,11.7 L27.9,10.8225 Z" fill="#E54011" sketch:type="MSShapeGroup"></path>
<path d="M28.1925,15.165 L31.1625,13.2525 L33.0525,19.62 L32.0625,21.645 L28.1925,15.165 Z" fill="#CA2317" sketch:type="MSShapeGroup"></path>
<path d="M23.76,22.725 L22.3425,5.4 L32.0625,21.645 L23.76,22.725 Z" fill="#B7DFF2" sketch:type="MSShapeGroup"></path>
<path d="M19.7325,27.1575 L23.76,22.725 L32.0625,21.645 L19.7325,27.1575 Z" fill="#E54011" sketch:type="MSShapeGroup"></path>
<path d="M0.1575,35.865 L19.7325,27.1575 L23.76,22.725 L17.37,22.0725 L0.1575,35.865 Z" fill="#FFCE33" sketch:type="MSShapeGroup"></path>
<path d="M0.9,28.755 L10.9575,27.225 L14.085,24.705 L12.555,24.03 L0.9,28.755 Z" fill="#D6B12D" sketch:type="MSShapeGroup"></path>
<path d="M4.5225,20.5425 L14.085,24.705 L17.37,22.0725 L4.5225,20.5425 Z" fill="#FFDE85" sketch:type="MSShapeGroup"></path>
<path d="M21.6225,11.6775 L20.4075,11.88 L17.37,22.0725 L20.655,20.0025 L21.6225,11.6775 Z" fill="#009EC6" sketch:type="MSShapeGroup"></path>
<path d="M23.4,18.2475 L20.655,20.0025 L22.3425,5.4 L23.4,18.2475 Z" fill="#5EAFCE" sketch:type="MSShapeGroup"></path>
<path d="M13.0275,13.05 L21.6225,11.6775 L22.005,8.28 L13.0275,13.05 Z" fill="#045972" sketch:type="MSShapeGroup"></path>
<path d="M12.105,5.085 L19.575,9.585 L22.005,8.28 L22.0725,7.8075 L12.105,5.085 Z" fill="#5A8591" sketch:type="MSShapeGroup"></path>
<path d="M13.5675,0.18 L20.3625,7.335 L22.0725,7.8075 L22.3425,5.4 L13.5675,0.18 Z" fill="#009EC6" sketch:type="MSShapeGroup"></path>
<path d="M17.37,22.0725 L23.4,18.2475 L23.76,22.725 L17.37,22.0725 Z" fill="#F39804" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

1
public/env.js vendored
View File

@ -1,3 +1,4 @@
window.env = { window.env = {
REACT_APP_API_URL: 'http://localhost:5000/api/v1', REACT_APP_API_URL: 'http://localhost:5000/api/v1',
REACT_APP_SSO_LOGOUT_URL: 'https://login.example.org/if/flow/default-invalidation-flow/'
}; };

View File

@ -0,0 +1 @@
Verwalte User und Gruppen

View File

@ -0,0 +1 @@
Ticketsystem

View File

@ -13,7 +13,6 @@ import { Login } from './modules/login';
import { Users } from './modules/users/Users'; import { Users } from './modules/users/Users';
import { AppSingle } from './modules/apps/AppSingle'; import { AppSingle } from './modules/apps/AppSingle';
import { Apps } from './modules/apps/Apps'; import { Apps } from './modules/apps/Apps';
import { DashboardLIT } from './modules/dashboard/DashboardLIT';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
function App() { function App() {
@ -53,7 +52,7 @@ function App() {
) : ( ) : (
<Layout> <Layout>
<Routes> <Routes>
<Route path="/dashboard" element={<DashboardLIT />} /> <Route path="/dashboard" element={<Dashboard />} />
{apps.map((app) => ( {apps.map((app) => (
<Route key={app.name} path={app.slug} element={<AppIframe app={app} />} /> <Route key={app.name} path={app.slug} element={<AppIframe app={app} />} />
))} ))}

View File

@ -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 { Disclosure, Menu, Transition } from '@headlessui/react';
import { MenuIcon, XIcon } from '@heroicons/react/outline'; import { MenuIcon, XIcon } from '@heroicons/react/outline';
import { useAuth } from 'src/services/auth'; import { useAuth } from 'src/services/auth';
@ -6,16 +6,10 @@ import Gravatar from 'react-gravatar';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import clsx from 'clsx'; import clsx from 'clsx';
import { useApps } from 'src/services/apps'; 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 = [ const navigation = [
{ name: 'Dashboard', to: '/dashboard', requiresAdmin: false }, { name: '', to: '/users', requiresAdmin: true },
{ name: 'Users', to: '/users', requiresAdmin: true }, // { name: 'Apps', to: '/apps', requiresAdmin: true },
{ name: 'Apps', to: '/apps', requiresAdmin: true },
]; ];
function classNames(...classes: any[]) { function classNames(...classes: any[]) {
@ -34,32 +28,15 @@ function filterNavigationByDashboardRole(isAdmin: boolean) {
interface HeaderProps {} interface HeaderProps {}
const HeaderLIT: React.FC<HeaderProps> = () => { const HeaderLIT: React.FC<HeaderProps> = () => {
const [currentUserModal, setCurrentUserModal] = useState(false);
const [currentUserId, setCurrentUserId] = useState(null);
const { logOut, currentUser, isAdmin } = useAuth(); const { logOut, currentUser, isAdmin } = useAuth();
const { pathname } = useLocation(); const { pathname } = useLocation();
const { apps, loadApps, appTableLoading } = useApps(); const { apps } = useApps();
const currentUserModalOpen = (id: any) => {
setCurrentUserId(id);
setCurrentUserModal(true);
};
const currentUserModalClose = () => {
setCurrentUserModal(false);
setCurrentUserId(null);
};
const navigationItems = filterNavigationByDashboardRole(isAdmin); const navigationItems = filterNavigationByDashboardRole(isAdmin);
const signOutUrl = useMemo(() => { const signOutUrl = useMemo(() => {
const { hostname } = window.location; // @ts-ignore
// If we are developing locally, we need to use the init cluster's public URL return window.env.REACT_APP_SSO_LOGOUT_URL;
if (hostname === 'localhost') {
return HYDRA_LOGOUT_URL;
}
return `https://${hostname.replace(/^dashboard/, 'sso')}/oauth2/sessions/logout`;
}, []); }, []);
return ( return (
@ -102,6 +79,21 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
{app.name} {app.name}
</Link> </Link>
))} ))}
{/* {navigationItems.map((item) => (
<Link
key={item.name}
to={item.to}
className={clsx(
'border-primary-50 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium litbutton',
{
'border-primary-500 litbutton-active hover:border-gray-300 inline-flex items-center px-1 pt-1 text-sm font-medium':
pathname.includes(item.to),
},
)}
>
{item.name}
</Link>
))} */}
</div> </div>
</div> </div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0"> <div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
@ -167,10 +159,6 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
</div> </div>
)} )}
</Disclosure> </Disclosure>
{currentUserModal && (
<UserModal open={currentUserModal} onClose={currentUserModalClose} userId={currentUserId} setUserId={_.noop} />
)}
</> </>
); );
}; };

View File

@ -1,2 +1 @@
export { default as Header } from './Header'; export { default as Header } from './HeaderLIT';
export { default as HeaderLIT } from './HeaderLIT';

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { HeaderLIT } from '../Header'; import { Header } from '../Header';
const Layout: React.FC = ({ children }) => { const Layout: React.FC = ({ children }) => {
return ( return (
<> <>
<HeaderLIT /> <Header />
{children} {children}
</> </>

View File

@ -1,5 +1,5 @@
export { Layout } from './Layout'; export { Layout } from './Layout';
export { Header, HeaderLIT } from './Header'; export { Header } from './Header';
export { Table } from './Table'; export { Table } from './Table';
export { Banner } from './Banner'; export { Banner } from './Banner';
export { Tabs } from './Tabs'; export { Tabs } from './Tabs';

View File

@ -1,9 +1,13 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useApps } from 'src/services/apps'; import { AppStatusEnum, useApps } from 'src/services/apps';
import { DashboardCardLIT } from './components/DashboardCard/DashboardCardLIT'; 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 { apps, loadApps, appTableLoading } = useApps();
const { isAdmin } = useAuth();
// Tell React to load the apps // Tell React to load the apps
useEffect(() => { useEffect(() => {
@ -21,9 +25,27 @@ export const DashboardLIT: React.FC = () => {
</div> </div>
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow"> <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 mb-10"> <div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 mb-10">
{!appTableLoading && apps.map((app) => <DashboardCardLIT app={app} key={app.name} />)} {!appTableLoading &&
apps
.filter((app) => UTILITY_APPS.indexOf(app.slug) === -1)
.map((app) => <DashboardCard app={app} key={app.name} />)}
</div> </div>
</div> </div>
{isAdmin && (
<div className="max-w-4xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow">
<div className="pb-4 border-b border-gray-200 sm:flex sm:items-center">
<h3 className="text-lg leading-6 font-medium text-gray-900">Administration</h3>
</div>
<dl className="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2">
{apps
.filter((app) => UTILITY_APPS.indexOf(app.slug) !== -1 && app.url !== null)
.map((app) => (
<DashboardUtility item={app} key={app.name} />
))}
</dl>
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
export const DashboardCardLIT: React.FC<any> = ({ app }: { app: any }) => { export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
return ( return (
<> <>
<Link <Link

View File

@ -1 +1 @@
export { DashboardCard } from './DashboardCard'; export { DashboardCard } from './DashboardCardLIT';

View File

@ -0,0 +1,36 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
export const DashboardUtility: React.FC<any> = ({ item }: { item: any }) => {
const [content, setContent] = useState('');
useEffect(() => {
fetch(item.markdownSrc)
.then((res) => res.text())
.then((md) => {
return setContent(md);
})
.catch(() => {});
}, [item.markdownSrc]);
return (
<Link
to={`/${item.slug}`}
key={item.name}
rel="noreferrer"
className="bg-white rounded-lg overflow-hidden sm:p-2 flex items-center group"
>
<div className="w-16 h-16 flex items-center justify-center bg-primary-100 group-hover:bg-primary-200 transition-colors rounded-lg mr-4">
{item.icon && <item.icon className="h-6 w-6 text-primary-900" aria-hidden="true" />}
{item.assetSrc && <img className="h-6 w-6" src={item.assetSrc} alt={item.name} />}
</div>
<div>
<dt className="truncate text-sm leading-5 font-medium">{item.name}</dt>
<dd className="mt-1 text-gray-500 text-sm leading-5 font-normal">
<ReactMarkdown>{content}</ReactMarkdown>
</dd>
</div>
</Link>
);
};

View File

@ -1 +1 @@
export { DashboardUtility } from './DashboardUtility'; export { DashboardUtility } from './DashboardUtilityLIT';

View File

@ -10,7 +10,7 @@ export const DASHBOARD_QUICK_ACCESS = [
]; ];
/** Apps that should not be shown on the dashboard */ /** 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" */ /** Apps that should be shown under "Utilities" */
export const UTILITY_APPS = ['monitoring']; export const UTILITY_APPS = ['authentik', 'zammad'];

View File

@ -1 +1 @@
export { Dashboard } from './Dashboard'; export { Dashboard } from './DashboardLIT';

View File

@ -53,6 +53,11 @@ export const Users: React.FC = () => {
const columns: any = React.useMemo( const columns: any = React.useMemo(
() => [ () => [
{
Header: 'Username',
accessor: 'preferredUsername',
width: 'auto',
},
{ {
Header: 'Name', Header: 'Name',
accessor: 'name', accessor: 'name',
@ -75,11 +80,12 @@ export const Users: React.FC = () => {
if (isAdmin) { if (isAdmin) {
return ( return (
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity"> <div className="text-right lg:opacity-0 transition-opacity">
<button <button
disabled
onClick={() => configureModalOpen(row.original.id)} onClick={() => configureModalOpen(row.original.id)}
type="button" type="button"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray bg-gray hover:bg-gray focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
> >
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" /> <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Configure Configure
@ -109,17 +115,19 @@ export const Users: React.FC = () => {
{isAdmin && ( {isAdmin && (
<div className="mt-3 sm:mt-0 sm:ml-4"> <div className="mt-3 sm:mt-0 sm:ml-4">
<button <button
disabled
onClick={() => configureModalOpen(null)} onClick={() => configureModalOpen(null)}
type="button" type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 " className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
> >
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" /> <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new user Add new user
</button> </button>
<button <button
disabled
onClick={() => setMultipleUsersModal(true)} onClick={() => setMultipleUsersModal(true)}
type="button" type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800" className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
> >
<ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" /> <ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new users Add new users