Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/platform/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# NextAuth
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_COOKIE_DOMAIN=.airqo.net

# App URLs
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Expand Down
2 changes: 1 addition & 1 deletion src/platform/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withAuth } from 'next-auth/middleware';
const isProduction = process.env.NODE_ENV === 'production';
const sessionCookieName = isProduction
? '__Secure-next-auth.session-token'
: 'analytics.next-auth.session-token';
: 'next-auth.session-token';

export default withAuth(
function middleware() {
Expand Down
18 changes: 18 additions & 0 deletions src/platform/src/next-auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ type SessionUser = NonNullable<DefaultSession['user']> & {
_id?: string;
firstName?: string;
lastName?: string;
userName?: string;
organization?: string;
privilege?: string;
country?: string;
phoneNumber?: string;
};

declare module 'next-auth' {
Expand All @@ -18,6 +23,12 @@ declare module 'next-auth' {
name?: string | null;
image?: string | null;
authMethods?: AuthMethods;
userName?: string;
organization?: string;
privilege?: string;
country?: string;
phoneNumber?: string;
exp?: number;
}

interface Session extends DefaultSession {
Expand All @@ -36,5 +47,12 @@ declare module 'next-auth/jwt' {
accessToken?: string;
expiresAt?: string;
authMethods?: AuthMethods;
userName?: string;
organization?: string;
privilege?: string;
country?: string;
phoneNumber?: string;
image?: string;
exp?: number;
}
}
23 changes: 14 additions & 9 deletions src/platform/src/shared/hooks/useLogout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { signOut } from 'next-auth/react';
import { useDispatch } from 'react-redux';
import { clearUser, setLoggingOut } from '@/shared/store/userSlice';
import { persistor } from '@/shared/store';
import { useRouter } from 'next/navigation';
import { useSelector } from 'react-redux';
import { selectLoggingOut } from '@/shared/store/selectors';
import { useCallback } from 'react';
Expand All @@ -19,7 +18,6 @@ const OAUTH_SIGNED_OUT_FLAG = 'airqo:oauth-signed-out';

export const useLogout = (callbackUrl?: string) => {
const dispatch = useDispatch();
const router = useRouter();
const isLoggingOut = useSelector(selectLoggingOut);
const { cache, mutate } = useSWRConfig();
const queryClient = useQueryClient();
Expand Down Expand Up @@ -118,14 +116,21 @@ export const useLogout = (callbackUrl?: string) => {
// Clear persisted Redux data
await persistor.purge();

// Sign out from NextAuth and redirect
await signOut({ callbackUrl: callbackUrl || '/user/login' });
// Sign out from NextAuth (clear cookie server-side) then force full page reload
await signOut({ redirect: false });
window.location.href = callbackUrl || '/user/login';
} catch (error) {
logger.error('Logout error in useLogout', error);
// Reset logging out state on error
dispatch(setLoggingOut(false));
// Fallback redirect
router.push(callbackUrl || '/user/login');
// signOut failed — cookie may still be set. Attempt fallback form-based
// signOut which is more reliable for cookie clearing, then navigate.
try {
await signOut({ redirect: false });
} catch {
// Both attempts failed — clear state and let the user know
dispatch(setLoggingOut(false));
return;
}
window.location.href = callbackUrl || '/user/login';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} finally {
sharedIsLoggingOut = false;
sharedLogoutPromise = null;
Expand All @@ -134,7 +139,7 @@ export const useLogout = (callbackUrl?: string) => {

sharedLogoutPromise = runLogout();
await sharedLogoutPromise;
}, [cache, callbackUrl, dispatch, isLoggingOut, mutate, queryClient, router]);
}, [cache, callbackUrl, dispatch, isLoggingOut, mutate, queryClient]);

return logout;
};
109 changes: 95 additions & 14 deletions src/platform/src/shared/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,55 @@ import {
import type { AuthMethods } from '@/shared/types/api';

const isProduction = process.env.NODE_ENV === 'production';
const configuredCookieDomain =
process.env.NEXTAUTH_COOKIE_DOMAIN?.trim() || undefined;

const getCookieDomain = () => {
if (!configuredCookieDomain) {
return undefined;
}

const referenceUrl = process.env.NEXTAUTH_URL;
if (!referenceUrl) {
return configuredCookieDomain;
}

try {
const host = new URL(referenceUrl).hostname.toLowerCase();
const normalizedDomain = configuredCookieDomain.replace(/^\./, '').toLowerCase();
const hostMatches =
host === normalizedDomain || host.endsWith(`.${normalizedDomain}`);

if (hostMatches) {
return configuredCookieDomain;
}

console.warn(
'[NextAuth] NEXTAUTH_COOKIE_DOMAIN does not match NEXTAUTH_URL host; disabling cookie domain override.',
{ configuredCookieDomain, host }
);
return undefined;
} catch {
console.warn(
'[NextAuth] Invalid NEXTAUTH_URL while validating cookie domain; disabling cookie domain override.',
{ configuredCookieDomain, referenceUrl }
);
return undefined;
}
};

const cookieDomain = getCookieDomain();
const cookieOptions = {
httpOnly: true,
sameSite: 'lax' as const,
path: '/',
secure: isProduction,
domain: cookieDomain,
};

const sessionCookieName = isProduction
? '__Secure-next-auth.session-token'
: 'analytics.next-auth.session-token';
: 'next-auth.session-token';

const isJwtLikeToken = (token: string): boolean =>
token.split('.').length === 3;
Expand Down Expand Up @@ -119,16 +165,35 @@ export const authOptions: any = {
.join(' ')
.trim();

let decoded: Record<string, unknown> | null = null;
try {
const parts = oauthToken.split('.');
if (parts.length === 3) {
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
decoded = JSON.parse(Buffer.from(padded, 'base64').toString('utf-8'));
}
} catch {
decoded = null;
}

return {
id: profile._id,
email: profile.email,
name: fullName || profile.email,
firstName: profile.firstName,
lastName: profile.lastName,
image: profile.profilePicture,
_id: profile._id,
accessToken: oauthToken,
authMethods: normalizeAuthMethods(profile.authMethods),
firstName: profile.firstName,
lastName: profile.lastName,
userName: (decoded?.userName as string) || profile.email,
organization: (decoded?.organization as string) || '',
privilege: (decoded?.privilege as string) || '',
country: (decoded?.country as string) || '',
phoneNumber: (decoded?.phoneNumber as string) || '',
expiresAt: (decoded?.expiresAt as string) || '',
exp: (decoded?.exp as number) || 0,
};
}

Expand All @@ -150,18 +215,24 @@ export const authOptions: any = {
password,
});

// Return user object for NextAuth
// Return user object for NextAuth with all fields for cross-app SSO
return {
id: loginData._id,
_id: loginData._id,
email: loginData.email,
name: `${loginData.firstName} ${loginData.lastName}`,
firstName: loginData.firstName,
lastName: loginData.lastName,
image: loginData.profilePicture,
_id: loginData._id,
accessToken: normalizeOAuthAccessToken(loginData.token),
expiresAt: loginData.expiresAt,
authMethods: normalizeAuthMethods(loginData.authMethods),
firstName: loginData.firstName,
lastName: loginData.lastName,
userName: loginData.userName,
organization: loginData.organization,
privilege: loginData.privilege,
country: loginData.country,
phoneNumber: loginData.phoneNumber || '',
exp: 0,
};
} catch (error: any) {
// Enhanced error handling to include status and full response data
Expand All @@ -181,12 +252,7 @@ export const authOptions: any = {
cookies: {
sessionToken: {
name: sessionCookieName,
options: {
httpOnly: true,
sameSite: 'lax' as const,
path: '/',
secure: isProduction,
},
options: cookieOptions,
},
},
pages: {
Expand All @@ -210,6 +276,13 @@ export const authOptions: any = {
token.firstName = user.firstName;
token.lastName = user.lastName;
token.authMethods = normalizeAuthMethods(user.authMethods);
token.userName = user.userName || user.email;
token.organization = user.organization || '';
token.privilege = user.privilege || '';
token.country = user.country || '';
token.phoneNumber = user.phoneNumber || '';
token.image = user.image;
token.exp = user.exp || 0;
}

if (trigger === 'update' && session) {
Expand Down Expand Up @@ -252,9 +325,17 @@ export const authOptions: any = {
(session as any).expiresAt = expiresAt;
(session as any).authMethods = authMethods;
if (session.user) {
(session.user as any)._id = (token as any)._id;
(session.user as any)._id = (token as any)._id || (token as any).id;
(session.user as any).firstName = (token as any).firstName;
(session.user as any).lastName = (token as any).lastName;
(session.user as any).userName = (token as any).userName || (session.user as any).email;
(session.user as any).organization = (token as any).organization || '';
(session.user as any).privilege = (token as any).privilege || '';
(session.user as any).country = (token as any).country || '';
(session.user as any).phoneNumber = (token as any).phoneNumber || '';
if ((token as any).image) {
(session.user as any).image = (token as any).image;
}
}
return session;
},
Expand Down
1 change: 1 addition & 0 deletions src/vertex/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_COOKIE_DOMAIN=.airqo.net

NEXT_PUBLIC_SLACK_BOT_TOKEN=
NEXT_PUBLIC_SLACK_CHANNEL=notifs-airqo-netmanager-web
Expand Down
10 changes: 8 additions & 2 deletions src/vertex/app/api/auth/[...nextauth]/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export const options: NextAuthOptions = {
id: profile._id,
email: profile.email,
name: `${profile.firstName} ${profile.lastName}`.trim() || profile.email,
image: profile.profilePicture || decoded?.profilePicture || '',
userName: profile.userName || decoded?.userName || profile.email,
accessToken: oauthToken,
organization: profile.organization || decoded?.organization || '',
Expand Down Expand Up @@ -261,6 +262,7 @@ export const options: NextAuthOptions = {
id: decoded._id,
email: decoded.email,
name: `${decoded.firstName} ${decoded.lastName}`,
image: decoded.profilePicture || '',
userName: decoded.userName,
accessToken: loginResponse.token,
organization: decoded.organization,
Expand Down Expand Up @@ -295,8 +297,8 @@ export const options: NextAuthOptions = {
cookies: {
sessionToken: {
name: isProduction
? '__Secure-next-auth.session-token-v2'
: 'next-auth.session-token-v2',
? '__Secure-next-auth.session-token'
: 'next-auth.session-token',
options: cookieOptions,
},
},
Expand All @@ -314,6 +316,7 @@ export const options: NextAuthOptions = {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token._id = (user._id as string | undefined) || user.id;
token.accessToken = user.accessToken;
token.userName = user.userName;
token.organization = user.organization;
Expand All @@ -323,6 +326,7 @@ export const options: NextAuthOptions = {
token.country = user.country;
token.timezone = user.timezone;
token.phoneNumber = user.phoneNumber;
token.image = user.image ?? undefined;
token.exp = user.exp;
}
return token;
Expand All @@ -337,6 +341,7 @@ export const options: NextAuthOptions = {
session.user = {
...session.user,
id: token.id as string,
_id: (token._id as string) || (token.id as string),
accessToken: token.accessToken as string,
userName: token.userName as string,
organization: token.organization as string,
Expand All @@ -346,6 +351,7 @@ export const options: NextAuthOptions = {
country: token.country as string,
timezone: token.timezone as string,
phoneNumber: token.phoneNumber as string,
image: (token.image as string) || '',
exp: token.exp,
};
}
Expand Down
9 changes: 4 additions & 5 deletions src/vertex/core/hooks/useLogout.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { signOut } from 'next-auth/react';
import { logout as clearUser, setLoggingOut } from '@/core/redux/slices/userSlice';
import { useRouter, usePathname } from 'next/navigation';
import { usePathname } from 'next/navigation';
import { useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { clearSessionData } from '../utils/sessionManager';
Expand All @@ -21,7 +21,6 @@ let sharedIsLoggingOut = false;
*/
export const useLogout = (callbackUrl?: string) => {
const dispatch = useAppDispatch();
const router = useRouter();
const pathname = usePathname();
const queryClient = useQueryClient();
const isLoggingOut = useAppSelector((state) => state.user.isLoggingOut);
Expand Down Expand Up @@ -66,11 +65,11 @@ export const useLogout = (callbackUrl?: string) => {
await persistor.purge();

await signOut({ redirect: false });
router.push(callbackUrl || '/login');
window.location.href = callbackUrl || '/login';
} catch (error) {
logger.error('Logout error:', { error });
dispatch(setLoggingOut(false));
router.push(callbackUrl || '/login');
window.location.href = callbackUrl || '/login';
} finally {
sharedIsLoggingOut = false;
sharedLogoutPromise = null;
Expand All @@ -79,7 +78,7 @@ export const useLogout = (callbackUrl?: string) => {

sharedLogoutPromise = runLogout();
await sharedLogoutPromise;
}, [isLoggingOut, dispatch, queryClient, router, pathname, userDetails, callbackUrl]);
}, [isLoggingOut, dispatch, queryClient, pathname, userDetails, callbackUrl]);

return logout;
};
4 changes: 2 additions & 2 deletions src/vertex/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { NextResponse } from "next/server";

const isProduction = process.env.NODE_ENV === "production";
const sessionCookieName = isProduction
? "__Secure-next-auth.session-token-v2"
: "next-auth.session-token-v2";
? "__Secure-next-auth.session-token"
: "next-auth.session-token";

export default withAuth(
function middleware(req) {
Expand Down
Loading
Loading