Skip to content

Commit

Permalink
feat(webapp): use supabase auth token to make API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
anupcowkur committed Nov 20, 2023
1 parent 06dbd0e commit 543d1c6
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 25 deletions.
14 changes: 8 additions & 6 deletions measure-web-app/app/[teamId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import React, { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/navigation';
import TeamSwitcher from "../components/team_switcher";
import { getAccessTokenOrRedirectToAuth, listenToAuthStateChangesAndUpdateCookies, logoutIfAuthError } from "../utils/auth_utils";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {

listenToAuthStateChangesAndUpdateCookies()

enum TeamsApiStatus {
Loading,
Success,
Expand Down Expand Up @@ -44,16 +46,16 @@ export default function DashboardLayout({
];

const [teamsApiStatus, setTeamsApiStatus] = useState(TeamsApiStatus.Loading);
const [authToken, setAuthToken] = useState("abcde123");
const [teams, setTeams] = useState(emptyTeams);
const [selectedTeam, setSelectedTeam] = useState(teams[0].id)

const pathName = usePathname();
const router = useRouter();

const getTeams = async (authToken:string) => {
const getTeams = async () => {
setTeamsApiStatus(TeamsApiStatus.Loading)

const authToken = await getAccessTokenOrRedirectToAuth(router)
const origin = "https://frosty-fog-7165.fly.dev"
const opts = {
headers: {
Expand All @@ -62,9 +64,9 @@ export default function DashboardLayout({
};

const res = await fetch(`${origin}/teams`, opts);

if(!res.ok) {
setTeamsApiStatus(TeamsApiStatus.Error)
logoutIfAuthError(router, res)
return
}

Expand All @@ -75,8 +77,8 @@ export default function DashboardLayout({
}

useEffect(() => {
getTeams(authToken)
}, [authToken]);
getTeams()
}, []);

const onTeamChanged = (item:string) => {
const selectedTeamId = teams.find((e) => e.name === item)!.id
Expand Down
20 changes: 14 additions & 6 deletions measure-web-app/app/[teamId]/overview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import Dropdown from "@/app/components/dropdown";
import FilterPill from "@/app/components/filter_pill";
import UserFlow from "@/app/components/user_flow";
import MetricsOverview from '@/app/components/metrics_overview';
import { getAccessTokenOrRedirectToAuth, logoutIfAuthError } from '@/app/utils/auth_utils';
import { useRouter } from 'next/navigation';

export default function Overview({ params }: { params: { teamId: string } }) {
const router = useRouter()

enum AppsApiStatus {
Loading,
Success,
Expand Down Expand Up @@ -43,9 +47,10 @@ export default function Overview({ params }: { params: { teamId: string } }) {
setFormattedEndDate(new Date(endDate).toLocaleDateString());
}, [startDate, endDate]);

const getApps = async (authToken:string, teamId:string, ) => {
const getApps = async (teamId:string, ) => {
setAppsApiStatus(AppsApiStatus.Loading)

const authToken = await getAccessTokenOrRedirectToAuth(router)
const origin = process.env.NEXT_PUBLIC_API_BASE_URL
const opts = {
headers: {
Expand All @@ -56,6 +61,7 @@ export default function Overview({ params }: { params: { teamId: string } }) {
const res = await fetch(`${origin}/teams/${teamId}/apps`, opts);
if(!res.ok) {
setAppsApiStatus(AppsApiStatus.Error)
logoutIfAuthError(router, res)
return
}
const data = await res.json()
Expand All @@ -66,12 +72,13 @@ export default function Overview({ params }: { params: { teamId: string } }) {
}

useEffect(() => {
getApps("abcde123", params.teamId)
getApps(params.teamId)
}, []);

const getFilters = async (authToken:string, appId:string, ) => {
const getFilters = async (appId:string, ) => {
setFiltersApiStatus(FiltersApiStatus.Loading)

const authToken = await getAccessTokenOrRedirectToAuth(router)
const origin = process.env.NEXT_PUBLIC_API_BASE_URL
const opts = {
headers: {
Expand All @@ -81,6 +88,7 @@ export default function Overview({ params }: { params: { teamId: string } }) {

const res = await fetch(`${origin}/apps/${appId}/filters`, opts);
if(!res.ok) {
logoutIfAuthError(router, res)
setFiltersApiStatus(FiltersApiStatus.Error)
return
}
Expand All @@ -92,7 +100,7 @@ export default function Overview({ params }: { params: { teamId: string } }) {
}

useEffect(() => {
getFilters("abcde123", selectedApp)
getFilters(selectedApp)
}, [selectedApp]);

return (
Expand Down Expand Up @@ -121,9 +129,9 @@ export default function Overview({ params }: { params: { teamId: string } }) {
</div>
<div className="py-8" />
{appsApiStatus === AppsApiStatus.Success && filtersApiStatus === FiltersApiStatus.Error && <p className="text-lg font-display">Error fetching filters, please refresh page or select a different app to try again</p>}
{appsApiStatus === AppsApiStatus.Success && filtersApiStatus === FiltersApiStatus.Success && <UserFlow authToken="abcde123" appId={selectedApp} startDate={startDate} endDate={endDate} appVersion={selectedVersion} />}
{appsApiStatus === AppsApiStatus.Success && filtersApiStatus === FiltersApiStatus.Success && <UserFlow appId={selectedApp} startDate={startDate} endDate={endDate} appVersion={selectedVersion} />}
<div className="py-8" />
{appsApiStatus === AppsApiStatus.Success && filtersApiStatus === FiltersApiStatus.Success && <MetricsOverview authToken="abcde123" appId={selectedApp} startDate={startDate} endDate={endDate} appVersion={selectedVersion} />}
{appsApiStatus === AppsApiStatus.Success && filtersApiStatus === FiltersApiStatus.Success && <MetricsOverview appId={selectedApp} startDate={startDate} endDate={endDate} appVersion={selectedVersion} />}
</div>
)
}
15 changes: 10 additions & 5 deletions measure-web-app/app/components/metrics_overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import InfoCircleAppAdoption from './info_circle_app_adoption';
import InfoCircleAppSize from './info_circle_app_size';
import InfoCircleExceptionRate from './info_circle_exception_rate';
import InfoCircleAppStartTime from './info_circle_app_start_time';
import { getAccessTokenOrRedirectToAuth, logoutIfAuthError } from '@/app/utils/auth_utils';
import { useRouter } from 'next/navigation';

interface MetricsOverviewProps {
authToken: string,
appId:string,
startDate:string,
endDate:string,
Expand All @@ -20,7 +21,7 @@ export enum MetricsApiStatus {
Error
}

const MetricsOverview: React.FC<MetricsOverviewProps> = ({ authToken, appId, startDate, endDate, appVersion }) => {
const MetricsOverview: React.FC<MetricsOverviewProps> = ({ appId, startDate, endDate, appVersion }) => {

const emptyData = {
"adoption": {
Expand Down Expand Up @@ -73,9 +74,12 @@ const MetricsOverview: React.FC<MetricsOverviewProps> = ({ authToken, appId, sta
const [data, setData] = useState(emptyData);
const [metricsApiStatus, setMetricsApiStatus] = useState(MetricsApiStatus.Loading);

const getData = async (authToken:string, appId:string, startDate:string, endDate:string, appVersion:string) => {
const router = useRouter()

const getData = async (appId:string, startDate:string, endDate:string, appVersion:string) => {
setMetricsApiStatus(MetricsApiStatus.Loading)

const authToken = await getAccessTokenOrRedirectToAuth(router)
const origin = process.env.NEXT_PUBLIC_API_BASE_URL
const opts = {
headers: {
Expand All @@ -89,6 +93,7 @@ const MetricsOverview: React.FC<MetricsOverviewProps> = ({ authToken, appId, sta

if(!res.ok) {
setMetricsApiStatus(MetricsApiStatus.Error)
logoutIfAuthError(router, res)
return
}

Expand All @@ -97,8 +102,8 @@ const MetricsOverview: React.FC<MetricsOverviewProps> = ({ authToken, appId, sta
}

useEffect(() => {
getData(authToken, appId, startDate, endDate, appVersion)
}, [authToken, appId, startDate, endDate, appVersion]);
getData(appId, startDate, endDate, appVersion)
}, [appId, startDate, endDate, appVersion]);

return (
<div className="flex flex-wrap gap-16 w-5/6">
Expand Down
17 changes: 11 additions & 6 deletions measure-web-app/app/components/user_flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

import React, { useState, useEffect } from 'react';
import { ResponsiveSankey } from '@nivo/sankey'
import { getAccessTokenOrRedirectToAuth, logoutIfAuthError } from '@/app/utils/auth_utils';
import { useRouter } from 'next/navigation';

interface UserFlowProps {
authToken: string,
appId:string,
startDate:string,
endDate:string,
appVersion:string,
}

const UserFlow: React.FC<UserFlowProps> = ({ authToken, appId, startDate, endDate, appVersion }) => {
const UserFlow: React.FC<UserFlowProps> = ({ appId, startDate, endDate, appVersion }) => {
enum JourneyApiStatus {
Loading,
Success,
Expand All @@ -38,9 +39,12 @@ const UserFlow: React.FC<UserFlowProps> = ({ authToken, appId, startDate, endDat
const [journeyApiStatus, setJourneyApiStatus] = useState(JourneyApiStatus.Loading);
const [data, setData] = useState(emptyData);

const getData = async (authToken:string, appId:string, startDate:string, endDate:string, appVersion:string) => {
const router = useRouter()

const getData = async (appId:string, startDate:string, endDate:string, appVersion:string) => {
setJourneyApiStatus(JourneyApiStatus.Loading)

const authToken = await getAccessTokenOrRedirectToAuth(router)
const origin = process.env.NEXT_PUBLIC_API_BASE_URL
const opts = {
headers: {
Expand All @@ -51,9 +55,10 @@ const UserFlow: React.FC<UserFlowProps> = ({ authToken, appId, startDate, endDat
const serverFormattedStartDate = new Date(startDate).toISOString()
const serverFormattedEndDate = new Date(endDate).toISOString()
const res = await fetch(`${origin}/apps/${appId}/journey?version=${appVersion}&from=${serverFormattedStartDate}&to=${serverFormattedEndDate}`, opts);

if(!res.ok) {
setJourneyApiStatus(JourneyApiStatus.Error)
logoutIfAuthError(router, res)
return
}

Expand All @@ -62,8 +67,8 @@ const UserFlow: React.FC<UserFlowProps> = ({ authToken, appId, startDate, endDat
}

useEffect(() => {
getData(authToken, appId, startDate, endDate, appVersion)
}, [authToken, appId, startDate, endDate, appVersion]);
getData(appId, startDate, endDate, appVersion)
}, [appId, startDate, endDate, appVersion]);

return (
<div className="flex items-center justify-center border border-black text-black font-sans text-sm w-5/6 h-screen">
Expand Down
58 changes: 58 additions & 0 deletions measure-web-app/app/utils/auth_utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createClient } from '@supabase/supabase-js'
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context';

// Utility function to listen to auth state changes and set and remove cookies accordingly
export function listenToAuthStateChangesAndUpdateCookies() {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
// delete cookies on sign out
const expires = new Date(0).toUTCString()
document.cookie = `sb-access-token=; path=/; expires=${expires}; SameSite=Lax; secure`
document.cookie = `sb-refresh-token=; path=/; expires=${expires}; SameSite=Lax; secure`
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
// set new access + refresh tokens as cookies on sign in/token refresh.
const maxAge = 100 * 365 * 24 * 60 * 60 // 100 years, never expires
document.cookie = `sb-access-token=${session!.access_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure`
document.cookie = `sb-refresh-token=${session!.refresh_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure`
}
})
}

// Utility function to try and access current access token. If session retrieval
// fails for any reason, logout will be called and the user will be redirected to auth
export async function getAccessTokenOrRedirectToAuth(router: AppRouterInstance) {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

const { data: {session}, error } = await supabase.auth.getSession()

if(error) {
await supabase.auth.signOut()
router.push('/auth/logout')
return null
}

return session!.access_token;
}

// Utility function to check if API reponse has an authentication error.
// If it does, logout will be called and the user will be redirected to auth
export async function logoutIfAuthError(router: AppRouterInstance, res: Response) {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

if(res.status === 401) {
await supabase.auth.signOut()
router.push('/auth/logout')
return
}
}
Loading

0 comments on commit 543d1c6

Please sign in to comment.