add-frames #1

Merged
philipp merged 22 commits from add-frames into fork 2022-11-02 10:25:45 +01:00
11 changed files with 171 additions and 46 deletions
Showing only changes of commit d1838267ea - Show all commits

View file

@ -3,6 +3,7 @@ HYDRA_CLIENT_SECRET=
HYDRA_AUTHORIZATION_BASE_URL="https://sso.example.org/application/o/authorize/" HYDRA_AUTHORIZATION_BASE_URL="https://sso.example.org/application/o/authorize/"
HYDRA_PUBLIC_URL="https://sso.example.org/application/o/" HYDRA_PUBLIC_URL="https://sso.example.org/application/o/"
TOKEN_URL="https://sso.example.org/application/o/token/" TOKEN_URL="https://sso.example.org/application/o/token/"
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

1
backend/.gitignore vendored
View file

@ -8,3 +8,4 @@ __pycache__
.envrc .envrc
.direnv .direnv
run_app.local.sh run_app.local.sh
*.db

11
backend/Makefile Normal file
View 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

View file

@ -1,21 +1,7 @@
"""Everything to do with Apps""" """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 database import db
from .models import App from .models import App
# import helpers.kubernetes as k8s
DEFAULT_APP_SUBDOMAINS = {
"nextcloud": "files",
"wordpress": "www",
"monitoring": "grafana",
}
class LITApp(App): class LITApp(App):
""" """

View file

@ -30,38 +30,39 @@ def hydra_callback():
token = HydraOauth.get_token(state, code) token = HydraOauth.get_token(state, code)
user_info = HydraOauth.get_user_info() user_info = HydraOauth.get_user_info()
# Match Kratos identity with Hydra # Match Kratos identity with Hydra
identities = KratosApi.get("/identities") # identities = KratosApi.get("/identities")
identity = None # identity = None
for i in identities.json(): # for i in identities.json():
if i["traits"]["email"] == user_info["email"]: # if i["traits"]["email"] == user_info["email"]:
identity = i # identity = i
access_token = create_access_token( 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() # apps = App.query.all()
app_roles = [] # app_roles = []
for app in apps: # for app in apps:
tmp_app_role = AppRole.query.filter_by( # tmp_app_role = AppRole.query.filter_by(
user_id=identity["id"], app_id=app.id # user_id=identity["id"], app_id=app.id
).first() # ).first()
app_roles.append( # app_roles.append(
{ # {
"name": app.slug, # "name": app.slug,
"role_id": tmp_app_role.role_id if tmp_app_role else None, # "role_id": tmp_app_role.role_id if tmp_app_role else None,
} # }
) # )
return jsonify( return jsonify(
{ {
"accessToken": access_token, "accessToken": access_token,
"userInfo": { "userInfo": {
"id": identity["id"], "id": user_info["email"],
"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

@ -5,6 +5,7 @@ HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID")
HYDRA_CLIENT_SECRET = os.environ.get("HYDRA_CLIENT_SECRET") HYDRA_CLIENT_SECRET = os.environ.get("HYDRA_CLIENT_SECRET")
HYDRA_AUTHORIZATION_BASE_URL = os.environ.get("HYDRA_AUTHORIZATION_BASE_URL") HYDRA_AUTHORIZATION_BASE_URL = os.environ.get("HYDRA_AUTHORIZATION_BASE_URL")
TOKEN_URL = os.environ.get("TOKEN_URL") TOKEN_URL = os.environ.get("TOKEN_URL")
REDIRECT_URL = os.environ.get("REDIRECT_URL")
LOGIN_PANEL_URL = os.environ.get("LOGIN_PANEL_URL") LOGIN_PANEL_URL = os.environ.get("LOGIN_PANEL_URL")

View file

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

View file

@ -1,18 +1,25 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Toaster } from 'react-hot-toast'; 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 { Layout } from './components';
import { Dashboard } from './modules'; import { Dashboard } from './modules';
import { AppIframe } from './modules/dashboard/AppIframe'; import { AppIframe } from './modules/dashboard/AppIframe';
import { Login } from './modules/login';
import { LoginCallback } from './modules/login/LoginCallback';
import { useApps } from './services/apps'; import { useApps } from './services/apps';
import { useAuth } from './services/auth';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
function App() { function App() {
const host = window.location.hostname; const { authToken, currentUser, isAdmin } = useAuth();
const splitedDomain = host.split('.'); const redirectToLogin = !authToken || !currentUser?.app_roles;
splitedDomain.shift();
const ProtectedRoute = () => {
return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />;
};
const { apps, loadApps } = useApps(); const { apps, loadApps } = useApps();
useEffect(() => { useEffect(() => {
@ -33,6 +40,13 @@ function App() {
</Helmet> </Helmet>
<div className="app bg-gray-50 min-h-screen flex flex-col"> <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> <Layout>
<Routes> <Routes>
<Route path="/dashboard" element={<Dashboard />} /> <Route path="/dashboard" element={<Dashboard />} />
@ -42,6 +56,7 @@ function App() {
<Route path="*" element={<Navigate to="/dashboard" />} /> <Route path="*" element={<Navigate to="/dashboard" />} />
</Routes> </Routes>
</Layout> </Layout>
)}
{/* Place to load notifications */} {/* Place to load notifications */}
<div <div

View 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>
);
}

View 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>
);
}

View file

@ -0,0 +1 @@
export { Login } from './Login';