Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1ff2486
Enable hCaptcha for prod/staging when site key set
Codebmk Jun 3, 2026
edfa491
Add social auth section and OAuth helpers
Codebmk Jun 3, 2026
69b663b
Merge branch 'staging' into renable-hcaptcha-and-social-auth
Codebmk Jun 3, 2026
762208b
Update auth redirects, middleware, and API proxy
Codebmk Jun 4, 2026
7510be3
Enable hCaptcha in development
Codebmk Jun 4, 2026
9508585
Add NETWORK.VIEW to role permissions
Codebmk Jun 4, 2026
39c9899
Update changelog.md
Codebmk Jun 4, 2026
0de31e7
Correct file paths to follow vertex changelog convention.
Codebmk Jun 4, 2026
0486642
Merge branch 'staging' into renable-hcaptcha-and-social-auth
Codebmk Jun 5, 2026
f2e76bf
Merge branch 'staging' into renable-hcaptcha-and-social-auth
Codebmk Jun 7, 2026
ccbad26
Update changelog.md
Codebmk Jun 7, 2026
7a886a4
Update changelog.md
Codebmk Jun 7, 2026
1ce578e
Correct file paths to follow vertex changelog convention
Codebmk Jun 7, 2026
7012b61
OAuth failure banner may not be visible on the email step
Codebmk Jun 7, 2026
02e3d6d
Persist the selected provider before redirecting OAuth.
Codebmk Jun 7, 2026
70799d6
Fix NextAuth auth gating in src/vertex/middleware.ts
Codebmk Jun 7, 2026
8d5f5c9
Consolidate/mirror legacy redirects across Vertex Next.js configs
Codebmk Jun 7, 2026
b0612a7
Handle OAuth token handoff and middleware bypass
Codebmk Jun 7, 2026
7eda50d
Update changelog.md
Codebmk Jun 7, 2026
9eb0f12
Improve OAuth redirect handling in authProvider
Codebmk Jun 7, 2026
cfd1003
Update changelog.md
Codebmk Jun 7, 2026
47acf76
Integrate group onboarding checklist sync
Codebmk Jun 7, 2026
86ef263
Use secureApiProxy and auto-sync onboarding steps
Codebmk Jun 7, 2026
c9e6680
Add group hooks and use in welcome page
Codebmk Jun 7, 2026
4574438
Update changelog.md
Codebmk Jun 7, 2026
44c2f62
Add cleanup for setTimeout to prevent post-unmount execution.
Codebmk Jun 7, 2026
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
2 changes: 1 addition & 1 deletion src/vertex/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ NEXT_PUBLIC_SLACK_CHANNEL=notifs-airqo-netmanager-web
SLACK_WEBHOOK_URL=

NEXT_PUBLIC_API_TOKEN=
NEXT_PUBLIC_API_URL=https://staging-analytics.airqo.net/api/v2/
NEXT_PUBLIC_API_URL=https://staging-vertex.airqo.net/api/v2/
NEXT_PUBLIC_ENV=development
NEXT_PUBLIC_ANALYTICS_URL=https://staging-analytics.airqo.net
NEXT_PUBLIC_VERTEX_DESKTOP_WINDOWS_DOWNLOAD_URL=
Expand Down
291 changes: 198 additions & 93 deletions src/vertex/app/(authenticated)/home/page.tsx

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions src/vertex/app/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,81 @@

---

## Version 1.23.61
**Released:** June 07, 2026

### Centralized Onboarding State & API Abstraction

Migrated the onboarding checklist state from local storage to a centralized backend API for the Organization context, solving cross-device synchronization issues for enterprise users. (Note: Personal context continues to use local storage pending backend API rollout).

<details>
<summary><strong>Onboarding API Integration (4)</strong></summary>

- **Backend Synchronization:** Deprecated `localStorage` as the single source of truth for the organization-level onboarding checklist (`add-device`, `assign-cohort`, `set-visibility`). State is now inherently bound to the `Group` document.
- **Dynamic State Resolution:** The frontend now relies on the API to dynamically evaluate resource availability (e.g., existing devices or cohorts) and pre-populate completed steps during `GET /users/groups/:groupId` requests.
- **JIT Patching & Race Condition Fix:** Implemented serialized `PATCH /api/v1/users/groups/:groupId/onboarding` requests for missing manual steps to prevent NextAuth session race conditions and unexpected lockouts.
- **API Hook Abstraction:** Refactored direct API proxy invocations into strongly-typed custom React Query hooks (`useGroupDetails` and `useUpdateGroupOnboarding`) to centralize query management and reduce Axios instantiation overhead.

</details>

<details>
<summary><strong>Files Modified (3)</strong></summary>

- `app/(authenticated)/home/page.tsx` [MODIFIED]
- `core/apis/organizations.ts` [MODIFIED]
- `core/hooks/useGroups.ts` [MODIFIED]

</details>

---

## Version 1.23.60
**Released:** June 04, 2026

### Multi-Provider Social Auth, API Proxy & Role Permissions

Introduced comprehensive multi-provider social authentication, updated API proxy routing, enhanced hCaptcha environment support, and fixed missing network view permissions for system administrators.

<details>
<summary><strong>Social Authentication & Routing (4)</strong></summary>

- **Multi-Provider Social Auth**: Replaced the standalone Google auth component with a unified `SocialAuthSection` that supports sign-ins via Google, GitHub, LinkedIn, and X (Twitter).
- **OAuth Session Management**: Extended `oauth-session` utilities to track the user's last-used provider via localStorage and intelligently resolve post-login redirect paths using `getLastActiveModule()`.
- **OAuth Handoff Improvements**: Refactored `TokenHandoffHandler` in `authProvider.tsx` to prevent login UI flashing during OAuth redirects, optimized the redirect logic to eliminate duplicate page reloads when the user is already on the destination route, and configured `middleware.ts` to intelligently bypass server-side routing for seamless token consumption.
- **API Proxy Routing**: Updated the Next.js API proxy destination from `staging-analytics.airqo.net` to `staging-vertex.airqo.net` across both `next.config.js` and `next.config.mjs`.
- **Legacy Route Redirects**: Added Next.js config redirects to automatically route legacy `/user/home` to `/home` and `/user/login` to `/login`. Updated `middleware.ts` to simplify the `authorized` callback.

</details>

<details>
<summary><strong>Security & Permissions (2)</strong></summary>

- **Admin Role Permissions Fix**: Restored access to the Sensor Manufacturers admin panel by explicitly including the `PERMISSIONS.NETWORK.VIEW` right in the static `AIRQO_ADMIN` role definition.
- **Dynamic hCaptcha Environments**: Upgraded `isHCaptchaEnabled` to conditionally support hCaptcha on production, staging, and local development environments solely based on the presence of a valid site key.

</details>

<details>
<summary><strong>Files Modified (11)</strong></summary>

- `.env.example` [MODIFIED]
- `app/login/page.tsx` [MODIFIED]
- `components/features/auth/social-auth-section.tsx` [ADDED]
- `components/features/auth/google-auth-section.tsx` [DELETED]
- `core/auth/authProvider.tsx` [MODIFIED]
- `core/auth/oauth-session.ts` [MODIFIED]
- `core/permissions/constants.ts` [MODIFIED]
- `lib/envConstants.ts` [MODIFIED]
- `middleware.ts` [MODIFIED]
- `next.config.mjs` [MODIFIED]
- `next.config.js` [MODIFIED]

</details>

---



## Version 1.23.59
**Released:** June 04, 2026

Expand Down
24 changes: 20 additions & 4 deletions src/vertex/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { getLastActiveModule } from "@/core/utils/userPreferences";
import { ROUTE_LINKS } from "@/core/routes";
// import GoogleAuthSection from "@/components/features/auth/google-auth-section";
import SocialAuthSection from "@/components/features/auth/social-auth-section";
import { motion, AnimatePresence } from "framer-motion";


Expand Down Expand Up @@ -104,10 +105,24 @@ export default function LoginPage() {
setPlatform('other');
}

const authError = searchParams.get('error');
if (authError === 'oauth_failed') {
showBanner({
severity: 'error',
message: 'Social sign-in failed or was cancelled. Please try again.',
scoped: true,
});
// Remove only the error flag while preserving callbackUrl and other safe params
const params = new URLSearchParams(searchParams.toString());
params.delete('error');
const nextUrl = params.toString() ? `/login?${params.toString()}` : '/login';
window.history.replaceState({}, '', nextUrl);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return () => {
isMounted.current = false;
};
}, [dispatch]);
}, [dispatch, searchParams, showBanner]);


const onSubmit = useCallback(async (values: z.infer<typeof loginSchema>) => {
Expand Down Expand Up @@ -244,13 +259,14 @@ export default function LoginPage() {
</div>

<div className="flex flex-col">
{/* {step === 'email' && (
<GoogleAuthSection
{step === 'email' && (
<SocialAuthSection
mode="login"
disabled={isLoading}
className="mb-6"
callbackUrl={callbackUrl}
/>
)} */}
)}
Comment thread
Codebmk marked this conversation as resolved.

<Form {...form}>
<form
Expand Down
4 changes: 4 additions & 0 deletions src/vertex/app/types/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface Group {
grp_manager: UserDetails
cohorts?: string[]
organization_slug?: string
onboarding_checklist?: {
is_dismissed: boolean;
completed_steps: string[];
}
}

export interface CohortGroupsResponse {
Expand Down
4 changes: 4 additions & 0 deletions src/vertex/app/types/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export interface Group {
status: string;
role: Role;
userType: string;
onboarding_checklist?: {
is_dismissed: boolean;
completed_steps: string[];
};
}

export interface UserDetails {
Expand Down
71 changes: 0 additions & 71 deletions src/vertex/components/features/auth/google-auth-section.tsx

This file was deleted.

160 changes: 160 additions & 0 deletions src/vertex/components/features/auth/social-auth-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
'use client';

import { useCallback, useEffect, useState } from 'react';
import { FaGithub, FaLinkedinIn } from 'react-icons/fa';
import { FaXTwitter } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc';
import {
buildOAuthInitiationUrl,
getLastUsedOAuthProvider,
resolveOAuthRedirectAfterUrl,
setLastUsedOAuthProvider,
type SupportedSocialAuthProvider,
} from '@/core/auth/oauth-session';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import { cn } from '@/lib/utils';
import { useBanner } from '@/context/banner-context';
import { getLastActiveModule } from '@/core/utils/userPreferences';

interface SocialAuthSectionProps {
mode: 'login' | 'register';
disabled?: boolean;
className?: string;
callbackUrl?: string | null;
}

const SOCIAL_PROVIDERS: Array<{
provider: SupportedSocialAuthProvider;
label: string;
Icon: React.ComponentType<{ className?: string }>;
iconClassName?: string;
}> = [
{
provider: 'google',
label: 'Google',
Icon: FcGoogle,
},
{
provider: 'github',
label: 'GitHub',
Icon: FaGithub,
iconClassName: 'text-slate-950 dark:text-white',
},
{
provider: 'linkedin',
label: 'LinkedIn',
Icon: FaLinkedinIn,
iconClassName: 'text-[#0A66C2]',
},
{
provider: 'twitter',
label: 'X',
Icon: FaXTwitter,
iconClassName: 'text-slate-950 dark:text-white',
},
];

export default function SocialAuthSection({
mode,
disabled = false,
className,
callbackUrl,
}: SocialAuthSectionProps) {
const { showBanner } = useBanner();
const actionLabel = mode === 'register' ? 'Continue with' : 'Sign in with';
const lastModule = getLastActiveModule();
const fallbackUrl = lastModule === 'admin' ? '/admin/networks' : '/home';
const redirectPath = callbackUrl || fallbackUrl;
const [lastUsedProvider, setLastUsedProvider] =
useState<SupportedSocialAuthProvider | null>(null);

useEffect(() => {
setLastUsedProvider(getLastUsedOAuthProvider());
}, []);

const orderedProviders = lastUsedProvider
? [
...SOCIAL_PROVIDERS.filter(
({ provider }) => provider === lastUsedProvider
),
...SOCIAL_PROVIDERS.filter(
({ provider }) => provider !== lastUsedProvider
),
]
: SOCIAL_PROVIDERS;

const handleSocialAuth = useCallback(
(provider: SupportedSocialAuthProvider) => {
if (typeof window === 'undefined' || disabled) return;

const redirectAfter = resolveOAuthRedirectAfterUrl(redirectPath);
const queryParams: Record<string, string> = {};

if (redirectAfter) {
queryParams.redirect_after = redirectAfter;
}


try {
setLastUsedOAuthProvider(provider);
window.location.replace(buildOAuthInitiationUrl(provider, queryParams));
} catch (error) {
showBanner({
severity: 'error',
message: `Social sign-in unavailable. Please try again in a moment.`,
scoped: true,
});
console.error(`Failed to start ${provider} OAuth flow:`, error);
}
},
[disabled, redirectPath, showBanner]
);

return (
<div className={cn('w-full space-y-4', className)}>
<div className="grid grid-cols-4 gap-2">
{orderedProviders.map(({ provider, label, Icon, iconClassName }) => {
const isLastUsed = provider === lastUsedProvider;

return (
<button
key={provider}
type="button"
disabled={disabled}
onClick={() => handleSocialAuth(provider)}
aria-label={`${actionLabel} ${label}`}
title={`${actionLabel} ${label}`}
className={cn(
'relative flex h-10 items-center justify-center overflow-hidden rounded-md border bg-white px-1.5 text-gray-900 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary/40 disabled:cursor-not-allowed disabled:border-slate-300 disabled:bg-white disabled:text-gray-400 dark:bg-transparent dark:text-gray-100 dark:disabled:border-slate-700 dark:disabled:bg-transparent dark:disabled:text-gray-500',
isLastUsed
? 'border-primary/50 bg-primary/5 hover:bg-primary/10 dark:border-primary/50 dark:bg-slate-900'
: 'border-slate-300 hover:bg-slate-50 hover:text-gray-900 dark:border-slate-700 dark:hover:bg-slate-800 dark:hover:text-gray-100'
)}
>
<Icon
className={cn(
'h-4 w-4 transition-transform duration-200 sm:h-[17px] sm:w-[17px]',
iconClassName,
isLastUsed && 'scale-105'
)}
/>
{isLastUsed ? (
<span className="absolute right-1 top-1 rounded-full bg-primary/10 px-1 py-0 text-[8px] font-semibold uppercase tracking-[0.08em] text-primary dark:bg-primary/15 dark:text-primary-foreground">
Last used
</span>
) : null}
<span className="sr-only">{`${actionLabel} ${label}`}</span>
</button>
);
})}
</div>

<div className="flex items-center gap-4">
<span className="h-px flex-1 bg-border" />
<span className="text-xs font-bold uppercase tracking-[0.2em] text-muted-foreground">
Or
</span>
<span className="h-px flex-1 bg-border" />
</div>
</div>
);
}
Loading
Loading