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

Commit

Permalink
feat: UI Auth/Login (#41)
Browse files Browse the repository at this point in the history
* chore: rename hooks; cleanup

* chore: remove auth provider

* chore: rename  hooks back

* chore: forgot to stage renamed files

* feat(auth): wip

* chore: add include creds everywhere; create delete helper func

* chore: wip login

* chore: wip

* chore: wip

* chore: wip fix auth

* chore: load self data

* feat: load user profile

* chore: fix login redirect when auth not required

* chore: rename session, fix exhaustive deps issue

* fix: api calls

* chore: show name on hover

* chore: get images working in dev mode; opacity on user ring

* feat: implement logout

* feat: add known providers/icons

* chore: set session null on logout

* chore: rm credentials include

* fix: linter warning

* fix: loading of providers

* fix: login page

* fix: session infinite loop

* feat(api): add CSRF token support

* fix: show empty user if no imageURL

* fix: set headers

* fix: try to fix these loadeffect loops

* chore: disable refresh interval in swr

* fix(csrf): drop type constraints on setCsrf

* chore: make login dynamic import

Co-authored-by: George MacRorie <me@georgemac.com>
  • Loading branch information
markphelps and GeorgeMac authored Jan 20, 2023
1 parent 96b3a78 commit 86acc1c
Show file tree
Hide file tree
Showing 15 changed files with 525 additions and 139 deletions.
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
14 changes: 11 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ 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 Login = loadable(() => import('./app/auth/Login'));

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 @@ -110,11 +117,12 @@ export default function App() {
return (
<SWRConfig
value={{
refreshInterval: 10000, // 10 seconds
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 { 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;
};

useEffect(() => {
const loadProviders = 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)));
}
};

loadProviders();
}, [setProviders, setError]);

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

0 comments on commit 86acc1c

Please sign in to comment.