Skip to content
This repository has been archived by the owner on Apr 10, 2023. It is now read-only.

feat: UI Auth/Login #41

Merged
merged 39 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a10c20b
chore: rename hooks; cleanup
markphelps Jan 16, 2023
df414ae
chore: remove auth provider
markphelps Jan 16, 2023
de1deb3
Merge branch 'main' into hooks-cleanup
markphelps Jan 16, 2023
f388093
chore: rename hooks back
markphelps Jan 17, 2023
0a78af8
Merge branch 'hooks-cleanup' of https://github.com/flipt-io/flipt-ui …
markphelps Jan 17, 2023
da6bb65
chore: forgot to stage renamed files
markphelps Jan 17, 2023
032b24d
feat(auth): wip
markphelps Jan 17, 2023
3341896
Merge branch 'main' into login
markphelps Jan 17, 2023
bbaf690
chore: add include creds everywhere; create delete helper func
markphelps Jan 17, 2023
2e7fdd4
chore: wip login
markphelps Jan 17, 2023
2081096
chore: wip
markphelps Jan 18, 2023
35d9b79
Merge branch 'main' into login
markphelps Jan 18, 2023
686e4b0
chore: wip
markphelps Jan 18, 2023
28ec7bd
chore: wip fix auth
markphelps Jan 18, 2023
b1850a5
chore: load self data
markphelps Jan 18, 2023
cd11338
feat: load user profile
markphelps Jan 18, 2023
172d858
chore: fix login redirect when auth not required
markphelps Jan 18, 2023
fbc0081
chore: rename session, fix exhaustive deps issue
markphelps Jan 18, 2023
e78804b
fix: api calls
markphelps Jan 18, 2023
04a7c50
chore: show name on hover
markphelps Jan 18, 2023
fb8ce1d
chore: get images working in dev mode; opacity on user ring
markphelps Jan 18, 2023
559baf4
feat: implement logout
markphelps Jan 19, 2023
e2fe2ef
feat: add known providers/icons
markphelps Jan 19, 2023
158d53b
Merge branch 'main' into login
markphelps Jan 19, 2023
588943f
chore: set session null on logout
markphelps Jan 20, 2023
c29777d
Merge branch 'login' of https://github.com/flipt-io/flipt-ui into login
markphelps Jan 20, 2023
494c7b7
chore: rm credentials include
markphelps Jan 20, 2023
c539acf
Merge branch 'main' into login
markphelps Jan 20, 2023
d117a98
fix: linter warning
markphelps Jan 20, 2023
b2b3ed5
fix: loading of providers
markphelps Jan 20, 2023
6a15387
fix: login page
markphelps Jan 20, 2023
e153987
fix: session infinite loop
markphelps Jan 20, 2023
baf347c
feat(api): add CSRF token support
GeorgeMac Jan 20, 2023
10d07bc
fix: show empty user if no imageURL
markphelps Jan 20, 2023
225b9eb
fix: set headers
markphelps Jan 20, 2023
4aa0794
fix: try to fix these loadeffect loops
markphelps Jan 20, 2023
277156c
chore: disable refresh interval in swr
markphelps Jan 20, 2023
9155a09
fix(csrf): drop type constraints on setCsrf
GeorgeMac Jan 20, 2023
137d98d
chore: make login dynamic import
markphelps Jan 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!DOCTYPE html>
<html lang="en" className="h-full w-auto">
<html lang="en" class="h-full w-auto bg-white">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flipt</title>
</head>
<body className="h-full bg-white antialiased">
<body class="h-full antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
13 changes: 11 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import loadable from '@loadable/component';
import { createHashRouter, RouterProvider } from 'react-router-dom';
import { SWRConfig } from 'swr';
import Login from './app/auth/Login';
import ErrorLayout from './app/ErrorLayout';
import EditFlag from './app/flags/EditFlag';
import Evaluation from './app/flags/Evaluation';
Expand All @@ -10,12 +11,18 @@ import Layout from './app/Layout';
import NotFoundLayout from './app/NotFoundLayout';
import NewSegment from './app/segments/NewSegment';
import Segment, { segmentLoader } from './app/segments/Segment';
import SessionProvider from './components/SessionProvider';

const Flags = loadable(() => import('./app/flags/Flags'));
const Segments = loadable(() => import('./app/segments/Segments'));
const Console = loadable(() => import('./app/console/Console'));

const router = createHashRouter([
{
path: '/login',
element: <Login />,
errorElement: <ErrorLayout />
},
{
path: '/',
element: <Layout />,
Expand Down Expand Up @@ -76,7 +83,7 @@ const router = createHashRouter([
const apiURL = '/api/v1';

const fetcher = async (uri: String) => {
const res = await fetch(apiURL + uri, { credentials: 'include' });
const res = await fetch(apiURL + uri);

class StatusError extends Error {
info: string;
Expand Down Expand Up @@ -114,7 +121,9 @@ export default function App() {
fetcher
}}
>
<RouterProvider router={router} />
<SessionProvider>
<RouterProvider router={router} />
</SessionProvider>
</SWRConfig>
);
}
2 changes: 0 additions & 2 deletions src/app/ErrorLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Link, useNavigate, useRouteError } from 'react-router-dom';
import logoFlag from '~/assets/logo-flag.png';
import Footer from '~/components/Footer';

export default function ErrorLayout() {
const error = useRouteError() as Error;
Expand Down Expand Up @@ -45,7 +44,6 @@ export default function ErrorLayout() {
</div>
</div>
</main>
<Footer />
</div>
);
}
24 changes: 18 additions & 6 deletions src/app/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Navigate, Outlet } from 'react-router-dom';
import { useSession } from '~/data/hooks/session';
import ErrorNotification from '../components/ErrorNotification';
import { ErrorProvider } from '../components/ErrorProvider';
import Footer from '../components/Footer';
import Header from '../components/Header';
import Sidebar from '../components/Sidebar';

// const userNavigation = [{ name: "Sign out", href: "#" }];

export default function Layout() {
function InnerLayout() {
const { session } = useSession();
const [sidebarOpen, setSidebarOpen] = useState(false);

if (!session) {
return <Navigate to="/login" />;
}

return (
<ErrorProvider>
<>
<Sidebar setSidebarOpen={setSidebarOpen} sidebarOpen={sidebarOpen} />
<div className="flex min-h-screen flex-col md:pl-64">
<div className="flex min-h-screen flex-col bg-white md:pl-64">
<Header setSidebarOpen={setSidebarOpen} />

<main className="flex px-6 py-10">
Expand All @@ -24,6 +28,14 @@ export default function Layout() {
</main>
<Footer />
</div>
</>
);
}

export default function Layout() {
return (
<ErrorProvider>
<InnerLayout />
<ErrorNotification />
</ErrorProvider>
);
Expand Down
2 changes: 0 additions & 2 deletions src/app/NotFoundLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
} from '@heroicons/react/24/outline';
import { Link } from 'react-router-dom';
import logoFlag from '~/assets/logo-flag.png';
import Footer from '~/components/Footer';

const links = [
{
Expand Down Expand Up @@ -125,7 +124,6 @@ export default function NotFoundLayout() {
</div>
</div>
</main>
<Footer />
</div>
);
}
155 changes: 155 additions & 0 deletions src/app/auth/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {
faGitlab,
faGoogle,
faOpenid
} from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { toLower, upperFirst } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import logoFlag from '~/assets/logo-flag.png';
import { listAuthMethods } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSession } from '~/data/hooks/session';
import { AuthMethod, AuthMethodOIDC } from '~/types/Auth';

interface ILoginProvider {
displayName: string;
icon?: any;
}

const knownProviders: Record<string, ILoginProvider> = {
google: {
displayName: 'Google',
icon: faGoogle
},
gitlab: {
displayName: 'GitLab',
icon: faGitlab
},
auth0: {
displayName: 'Auth0'
}
};

export default function Login() {
const { session } = useSession();

const [providers, setProviders] = useState<
{
name: string;
authorize_url: string;
callback_url: string;
icon: any;
}[]
>([]);

const { setError, clearError } = useError();

const authorize = async (uri: string) => {
const res = await fetch(uri, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});

if (!res.ok || res.status !== 200) {
setError(new Error('Unable to authenticate: ' + res.text()));
return;
}

clearError();
const body = await res.json();
window.location.href = body.authorizeUrl;
};

const loadAvailableProviders = useCallback(async () => {
try {
const resp = await listAuthMethods();
// TODO: support alternative auth methods
const authOIDC = resp.methods.find(
(m: AuthMethod) => m.method === 'METHOD_OIDC' && m.enabled
) as AuthMethodOIDC;

if (!authOIDC) {
return;
}

const loginProviders = Object.entries(authOIDC.metadata.providers).map(
([k, v]) => {
k = toLower(k);
return {
name: knownProviders[k]?.displayName || upperFirst(k), // if we dont know the provider, just capitalize the first letter
authorize_url: v.authorize_url,
callback_url: v.callback_url,
icon: knownProviders[k]?.icon || faOpenid // if we dont know the provider icon, use the openid icon
};
}
);
setProviders(loginProviders);
} catch (err) {
setError(err instanceof Error ? err : Error(String(err)));
}
}, [setError]);

useEffect(() => {
loadAvailableProviders();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it would cause an endless loop of loading providers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i checked with console.log it only calls once, I'm wondering if its because its wrapped in a useCallback?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be because you're passing the loadAvailableProviders function as an arg below?

useEffect docs suggest the second argument is compared between each render.
If it doesn't change then the first argument function is not called again.
Seems to me that function will never change. Or maybe there is some overloaded alternative behaviour when a function is passed as second arg.

Seems you could also pass [] for the same effect:
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like more often than not you want state in your dependencies array passed to useEffect.
There are cases where you might pass a function. Though that seems to be when they're defined out of scope:
https://reacttraining.com/blog/when-to-use-functions-in-hooks-dependency-array/

(I admit as I kept reading this I started to glaze over and maybe missed something key)

}, [loadAvailableProviders]);

if (session) {
return <Navigate to="/" />;
}

return (
<>
<div className="flex min-h-screen flex-col justify-center sm:px-6 lg:px-8">
<main className="flex px-6 py-10">
<div className="w-full overflow-x-auto px-4 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<img
src={logoFlag}
alt="logo"
width={512}
height={512}
className="m-auto h-20 w-auto"
/>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Login to Flipt
</h2>
</div>

<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-sm">
<div className="py-8 px-4 sm:px-10">
<div className="mt-6 flex flex-col space-y-5">
{providers.map((provider) => (
<div key={provider.name}>
<a
href="#"
className="inline-flex w-full justify-center rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-500 shadow-sm hover:text-violet-500 hover:shadow-violet-300"
onClick={(e) => {
e.preventDefault();
authorize(provider.authorize_url);
}}
>
<span className="sr-only">
Sign in with {provider.name}
</span>
<FontAwesomeIcon
icon={provider.icon}
className="text-gray h-5 w-5"
aria-hidden={true}
/>
<span className="ml-2">With {provider.name}</span>
</a>
</div>
))}
</div>
</div>
</div>
</div>
</main>
</div>
</>
);
}
41 changes: 20 additions & 21 deletions src/app/console/Console.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,22 @@ export default function Console() {
const navigate = useNavigate();

const loadData = useCallback(async () => {
try {
const initialFlagList = (await listFlags()) as IFlagList;
const { flags } = initialFlagList;
const initialFlagList = (await listFlags()) as IFlagList;
const { flags } = initialFlagList;

setFlags(
flags.map((flag) => {
const status = flag.enabled ? 'active' : 'inactive';
setFlags(
flags.map((flag) => {
const status = flag.enabled ? 'active' : 'inactive';

return {
...flag,
status,
filterValue: flag.key,
displayValue: flag.name
};
})
);
clearError();
} catch (err) {
setError(err instanceof Error ? err : Error(String(err)));
}
}, [clearError, setError]);
return {
...flag,
status,
filterValue: flag.key,
displayValue: flag.name
};
})
);
}, []);

const handleSubmit = (values: IConsole) => {
const { flagKey, entityId, context } = values;
Expand All @@ -79,8 +74,12 @@ export default function Console() {
}, [response]);

useEffect(() => {
loadData();
}, [loadData]);
loadData()
.then(() => clearError())
.catch((err) => {
setError(err);
});
}, [clearError, loadData, setError]);

const initialvalues: IConsole = {
flagKey: selectedFlag?.key || '',
Expand Down
Loading