Skip to content

Commit

Permalink
lift to updated versions completed. authv5 implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
siddheshraze committed Feb 5, 2025
1 parent c23d565 commit 4d44ab5
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 235 deletions.
100 changes: 2 additions & 98 deletions frontend/app/api/auth/[[...nextauth]]/route.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,3 @@
import NextAuth, { AzureADProfile } from 'next-auth';
import AzureADProvider from 'next-auth/providers/azure-ad';
import { UserAuthRoles } from '@/config/macros';
import { SitesRDS, SitesResult } from '@/config/sqlrdsdefinitions/zones';
import ConnectionManager from '@/config/connectionmanager';
import MapperFactory from '@/config/datamapper';
import { handlers } from '@/auth';

const handler = NextAuth({
secret: process.env.NEXTAUTH_SECRET!,
providers: [
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID!,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
tenantId: process.env.AZURE_AD_TENANT_ID!,
authorization: { params: { scope: 'openid profile email user.Read' } }
})
],
session: {
strategy: 'jwt',
maxAge: 24 * 60 * 60 // 24 hours (you can adjust this value as needed)
},
callbacks: {
async signIn({ user, profile, email: signInEmail }) {
const azureProfile = profile as AzureADProfile;
const userEmail = user.email || signInEmail || azureProfile.preferred_username;
if (typeof userEmail !== 'string') {
console.error('User email is not a string:', userEmail);
return false; // Email is not a valid string, abort sign-in
}
if (userEmail) {
const connectionManager = ConnectionManager.getInstance();
let emailVerified, userStatus, userID;
try {
const query = `SELECT UserID, UserStatus FROM catalog.users WHERE Email = '${userEmail}' LIMIT 1`;
const results = await connectionManager.executeQuery(query);

// emailVerified is true if there is at least one result
emailVerified = results.length > 0;
if (!emailVerified) {
console.error('User email not found.');
return false;
}
userStatus = results[0].UserStatus;
userID = results[0].UserID;
} catch (e: any) {
console.error('Error fetching user status:', e);
throw new Error('Failed to fetch user status.');
}
user.userStatus = userStatus as UserAuthRoles;
user.email = userEmail;
const allSites = MapperFactory.getMapper<SitesRDS, SitesResult>('sites').mapData(await connectionManager.executeQuery(`SELECT * FROM catalog.sites`));
const allowedSites = MapperFactory.getMapper<SitesRDS, SitesResult>('sites').mapData(
await connectionManager.executeQuery(
`SELECT s.* FROM catalog.sites AS s JOIN catalog.usersiterelations AS usr ON s.SiteID = usr.SiteID WHERE usr.UserID = ?`,
[userID]
)
);
if (!allowedSites || !allSites) {
console.error('User does not have any allowed sites.');
return false;
}

user.sites = allowedSites;
user.allsites = allSites;
}
return true;
},

async jwt({ token, user }) {
// If this is the first time the JWT is issued, persist custom properties
if (user) {
token.userStatus = user.userStatus;
token.sites = user.sites;
token.allsites = user.allsites;
}
return token;
},

async session({ session, token }) {
if (typeof token.userStatus === 'string') {
session.user.userStatus = token.userStatus as UserAuthRoles;
} else {
session.user.userStatus = 'field crew' as UserAuthRoles; // default no admin permissions
}
if (token && token.allsites && Array.isArray(token.allsites)) {
session.user.allsites = token.allsites as SitesRDS[];
}
if (token && token.sites && Array.isArray(token.sites)) {
session.user.sites = token.sites as SitesRDS[];
}
return session;
}
},
pages: {
error: '/loginfailed'
}
});

export { handler as GET, handler as POST };
export const { GET, POST } = handlers;
39 changes: 39 additions & 0 deletions frontend/app/api/customsignin/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextResponse } from 'next/server';
import ConnectionManager from '@/config/connectionmanager';
import MapperFactory from '@/config/datamapper';
import { SitesRDS, SitesResult } from '@/config/sqlrdsdefinitions/zones';

export async function POST(req: Request) {
const { email } = await req.json();
const connectionManager = ConnectionManager.getInstance();

try {
const query = `SELECT UserID, UserStatus FROM catalog.users WHERE Email = ? LIMIT 1`;
const results = await connectionManager.executeQuery(query, [email]);

if (results.length === 0) {
return NextResponse.json({ error: 'User not found' }, { status: 401 });
}

const userID = results[0].UserID;
const userStatus = results[0].UserStatus;

const allSites = MapperFactory.getMapper<SitesRDS, SitesResult>('sites').mapData(await connectionManager.executeQuery(`SELECT * FROM catalog.sites`));

const allowedSites = MapperFactory.getMapper<SitesRDS, SitesResult>('sites').mapData(
await connectionManager.executeQuery(
`SELECT s.* FROM catalog.sites AS s JOIN catalog.usersiterelations AS usr ON s.SiteID = usr.SiteID WHERE usr.UserID = ?`,
[userID]
)
);

return NextResponse.json({
userStatus,
allSites,
allowedSites
});
} catch (error) {
console.error('Database error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
66 changes: 66 additions & 0 deletions frontend/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// auth.ts
import NextAuth from 'next-auth';
import { UserAuthRoles } from '@/config/macros';
import { SitesRDS } from '@/config/sqlrdsdefinitions/zones';
import MicrosoftEntraID from '@auth/core/providers/microsoft-entra-id';

export const { auth, handlers, signIn, signOut } = NextAuth({
secret: process.env.AUTH_SECRET!,
providers: [
MicrosoftEntraID({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
issuer: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}/v2.0`
})
],
session: {
strategy: 'jwt',
maxAge: 24 * 60 * 60 // 24 hours
},
callbacks: {
async signIn({ user, profile, email: signInEmail }) {
console.log('url: ', process.env.AUTH_URL);
const userEmail = user.email || signInEmail || profile?.preferred_username;
if (!userEmail) {
return false; // No email, reject sign-in
}
try {
const response = await fetch(`${process.env.NEXTAUTH_URL ?? 'http://localhost:3000'}/api/customsignin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: userEmail })
});

if (!response.ok) {
return false;
}

const data = await response.json();
user.userStatus = data.userStatus;
user.sites = data.allowedSites;
user.allsites = data.allSites;
} catch (error) {
console.error('Error fetching user data:', error);
return false;
}

return true;
},

async jwt({ token, user }) {
if (user) {
token.userStatus = user.userStatus;
token.sites = user.sites;
token.allsites = user.allsites;
}
return token;
},

async session({ session, token }) {
session.user.userStatus = token.userStatus as UserAuthRoles;
session.user.sites = token.sites as SitesRDS[];
session.user.allsites = token.allsites as SitesRDS[];
return session;
}
}
});
2 changes: 1 addition & 1 deletion frontend/components/client/loginfailure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const LoginFailed = () => {
const handleTryAgain = () => {
sessionStorage.clear();
localStorage.clear();
signOut({ callbackUrl: '/login' }).catch(console.error);
signOut({ redirectTo: '/login' }).catch(console.error);
};

return (
Expand Down
18 changes: 13 additions & 5 deletions frontend/components/loginlogout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { signIn, signOut, useSession } from 'next-auth/react';
// loginlogout.tsx
'use client';
import React from 'react';
import Avatar from '@mui/joy/Avatar';
import Box from '@mui/joy/Box';
Expand All @@ -8,14 +9,15 @@ import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded';
import LoginRoundedIcon from '@mui/icons-material/LoginRounded';
import CircularProgress from '@mui/joy/CircularProgress';
import { Skeleton } from '@mui/joy';
import { signIn, signOut, useSession } from 'next-auth/react';

export const LoginLogout = () => {
const { data: session, status } = useSession();

const handleRetryLogin = () => {
signIn('azure-ad', { callbackUrl: '/dashboard' }, { prompt: 'login' }).catch((error: any) => {
signIn('microsoft-entra-id', { redirectTo: '/dashboard' }).catch((error: any) => {
console.error('Login error:', error);
signOut({ callbackUrl: `/loginfailed?reason=${error.message}` })
signOut({ redirectTo: `/loginfailed?reason=${error.message}` })
.then(() => localStorage.clear())
.then(() => sessionStorage.clear());
});
Expand All @@ -31,7 +33,13 @@ export const LoginLogout = () => {
<Typography level="title-sm">Login to access</Typography>
<Typography level="body-xs">your information</Typography>
</Box>
<IconButton size="sm" variant="plain" color="neutral" onClick={handleRetryLogin} aria-label={'Login button'}>
<IconButton
size="sm"
variant="plain"
color="neutral"
onClick={() => handleRetryLogin()}
aria-label={'Login' + ' button'}
>
<LoginRoundedIcon />
</IconButton>
</Box>
Expand All @@ -57,7 +65,7 @@ export const LoginLogout = () => {
<Skeleton loading={status == 'loading'}>{session?.user?.email ? session?.user?.email : ''}</Skeleton>
</Typography>
</Box>
<IconButton size="sm" variant="plain" color="neutral" onClick={() => void signOut({ callbackUrl: '/login' })} aria-label={'Logout button'}>
<IconButton size="sm" variant="plain" color="neutral" onClick={() => void signOut({ redirectTo: '/login' })} aria-label={'Logout button'}>
{status == 'loading' ? <CircularProgress size={'lg'} /> : <LogoutRoundedIcon />}
</IconButton>
</Box>
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -950,8 +950,8 @@ export default function Sidebar(props: SidebarProps) {
{
plotSelectionRequired: plot === undefined,
censusSelectionRequired: census === undefined,
pathname,
isParentDataIncomplete
pathname: pathname ?? '',
isParentDataIncomplete: isParentDataIncomplete
},
item,
toggle,
Expand Down
36 changes: 18 additions & 18 deletions frontend/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,35 @@
* Allows the request to continue if no redirect conditions are met.
*/

import { getToken } from 'next-auth/jwt';
import { NextRequest, NextResponse } from 'next/server';
import { NextResponse, NextRequest } from 'next/server';
import { auth } from '@/auth';

export async function middleware(request: NextRequest) {
const session = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET
});
export default auth(async function middleware(request: NextRequest) {
const session = await auth(); // Fetch session once
const url = request.nextUrl.clone();
if (url.pathname.startsWith('/dashboard') || url.pathname.startsWith('/measurementshub') || url.pathname.startsWith('/fixeddatainput')) {
if (!session) {
// If user is not authenticated and tries to access protected routes, redirect to login

const isAuthenticated = !!session;
const isProtectedRoute = ['/dashboard', '/measurementshub', '/fixeddatainput'].some(route => url.pathname.startsWith(route));

if (isProtectedRoute && !isAuthenticated) {
// Redirect unauthenticated users trying to access protected routes
if (url.pathname !== '/login') {
url.pathname = '/login';
return NextResponse.redirect(url);
}
} else if (url.pathname === '/') {
// Redirect from home to dashboard if authenticated, or login if not
if (!session) {
url.pathname = '/login';
} else {
// Redirect from home to dashboard if authenticated, otherwise to login
if (isAuthenticated) {
url.pathname = '/dashboard';
} else {
url.pathname = '/login';
}
return NextResponse.redirect(url);
}

// Allow request to continue if no conditions are met
return NextResponse.next();
}
return NextResponse.next(); // Allow request to continue if no conditions are met
});

export const config = {
matcher: ['/', '/dashboard', '/measurementshub/:path*', '/fixeddatainput/:path*']
matcher: ['/', '/dashboard/:path*', '/measurementshub/:path*', '/fixeddatainput/:path*']
};
1 change: 1 addition & 0 deletions frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Loading

0 comments on commit 4d44ab5

Please sign in to comment.