add-frames #1
11 changed files with 171 additions and 46 deletions
|
@ -3,6 +3,7 @@ HYDRA_CLIENT_SECRET=
|
|||
HYDRA_AUTHORIZATION_BASE_URL="https://sso.example.org/application/o/authorize/"
|
||||
HYDRA_PUBLIC_URL="https://sso.example.org/application/o/"
|
||||
TOKEN_URL="https://sso.example.org/application/o/token/"
|
||||
REDIRECT_URL="https://example.org/login-callback"
|
||||
SECRET_KEY=
|
||||
LOAD_INCLUSTER_CONFIG=false
|
||||
DATABASE_URL=sqlite:///database.db
|
||||
|
|
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
|
@ -8,3 +8,4 @@ __pycache__
|
|||
.envrc
|
||||
.direnv
|
||||
run_app.local.sh
|
||||
*.db
|
||||
|
|
11
backend/Makefile
Normal file
11
backend/Makefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
clean:
|
||||
rm database.db
|
||||
flask db upgrade
|
||||
|
||||
demo:
|
||||
flask cli app create nextcloud Dateiablage "https://cloud.dev.local-it.cloud"
|
||||
flask cli app create vikunja Projekte "https://vikunja.dev.local-it.cloud"
|
||||
|
||||
run:
|
||||
flask run
|
|
@ -1,21 +1,7 @@
|
|||
"""Everything to do with Apps"""
|
||||
|
||||
import os
|
||||
import base64
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from database import db
|
||||
from .models import App
|
||||
# import helpers.kubernetes as k8s
|
||||
|
||||
|
||||
DEFAULT_APP_SUBDOMAINS = {
|
||||
"nextcloud": "files",
|
||||
"wordpress": "www",
|
||||
"monitoring": "grafana",
|
||||
}
|
||||
|
||||
class LITApp(App):
|
||||
"""
|
||||
|
|
|
@ -30,38 +30,39 @@ 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": identity["id"],
|
||||
"id": user_info["email"],
|
||||
"email": user_info["email"],
|
||||
"name": user_info["name"],
|
||||
"preferredUsername": user_info["preferred_username"],
|
||||
"app_roles": app_roles,
|
||||
# "app_roles": app_roles,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID")
|
|||
HYDRA_CLIENT_SECRET = os.environ.get("HYDRA_CLIENT_SECRET")
|
||||
HYDRA_AUTHORIZATION_BASE_URL = os.environ.get("HYDRA_AUTHORIZATION_BASE_URL")
|
||||
TOKEN_URL = os.environ.get("TOKEN_URL")
|
||||
REDIRECT_URL = os.environ.get("REDIRECT_URL")
|
||||
|
||||
LOGIN_PANEL_URL = os.environ.get("LOGIN_PANEL_URL")
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ class HydraOauth:
|
|||
@staticmethod
|
||||
def authorize():
|
||||
try:
|
||||
hydra = OAuth2Session(HYDRA_CLIENT_ID)
|
||||
hydra = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL)
|
||||
authorization_url, state = hydra.authorization_url(
|
||||
HYDRA_AUTHORIZATION_BASE_URL
|
||||
)
|
||||
|
|
23
src/App.tsx
23
src/App.tsx
|
@ -1,18 +1,25 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Layout } from './components';
|
||||
import { Dashboard } from './modules';
|
||||
import { AppIframe } from './modules/dashboard/AppIframe';
|
||||
import { Login } from './modules/login';
|
||||
import { LoginCallback } from './modules/login/LoginCallback';
|
||||
import { useApps } from './services/apps';
|
||||
import { useAuth } from './services/auth';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function App() {
|
||||
const host = window.location.hostname;
|
||||
const splitedDomain = host.split('.');
|
||||
splitedDomain.shift();
|
||||
const { authToken, currentUser, isAdmin } = useAuth();
|
||||
const redirectToLogin = !authToken || !currentUser?.app_roles;
|
||||
|
||||
const ProtectedRoute = () => {
|
||||
return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />;
|
||||
};
|
||||
|
||||
const { apps, loadApps } = useApps();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -33,6 +40,13 @@ function App() {
|
|||
</Helmet>
|
||||
|
||||
<div className="app bg-gray-50 min-h-screen flex flex-col">
|
||||
{redirectToLogin ? (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/login-callback" element={<LoginCallback />} />
|
||||
<Route path="*" element={<Navigate to="/login" />} />
|
||||
</Routes>
|
||||
) : (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
|
@ -42,6 +56,7 @@ function App() {
|
|||
<Route path="*" element={<Navigate to="/dashboard" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
)}
|
||||
|
||||
{/* Place to load notifications */}
|
||||
<div
|
||||
|
|
50
src/modules/login/Login.tsx
Normal file
50
src/modules/login/Login.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Login page that starts the OAuth2 authentication flow.
|
||||
*/
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { LockClosedIcon } from '@heroicons/react/solid';
|
||||
|
||||
import { performApiCall } from 'src/services/api';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function Login() {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/login',
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (data.authorizationUrl) {
|
||||
window.location.href = data.authorizationUrl;
|
||||
}
|
||||
} catch (e: any) {
|
||||
showToast('Something went wrong', ToastType.Error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<img className="lg:block" src="assets/logo.svg" alt="Stackspin" />
|
||||
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-dark hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
)}
|
||||
>
|
||||
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
|
||||
</span>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
58
src/modules/login/LoginCallback.tsx
Normal file
58
src/modules/login/LoginCallback.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function LoginCallback() {
|
||||
const currentURL = window.location.href;
|
||||
const indexOfQuestionMark = currentURL.indexOf('?');
|
||||
const params = currentURL.slice(indexOfQuestionMark);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { logIn } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
async function logInUser() {
|
||||
if (params.length > 2) {
|
||||
const res = await logIn(params);
|
||||
|
||||
// @ts-ignore
|
||||
if (!res.ok) {
|
||||
navigate('/login');
|
||||
showToast('Something went wrong, please try logging in again.', ToastType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logInUser();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<div className="flex justify-center items-center border border-transparent text-base font-medium rounded-md text-white transition ease-in-out duration-150">
|
||||
<svg
|
||||
className="animate-spin h-10 w-10 text-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle className="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path
|
||||
className="opacity-100"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-lg text-primary-600 mt-2">Logging You in, just a moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
1
src/modules/login/index.ts
Normal file
1
src/modules/login/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Login } from './Login';
|
Loading…
Reference in a new issue