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_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
1
backend/.gitignore
vendored
|
@ -8,3 +8,4 @@ __pycache__
|
||||||
.envrc
|
.envrc
|
||||||
.direnv
|
.direnv
|
||||||
run_app.local.sh
|
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"""
|
"""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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
23
src/App.tsx
23
src/App.tsx
|
@ -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
|
||||||
|
|
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