diff --git a/src/app.tsx b/src/app.tsx index df3aec4..6c846d9 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -2,6 +2,7 @@ import { MetaProvider, Title } from '@solidjs/meta'; import { Router } from '@solidjs/router'; import { FileRoutes } from '@solidjs/start/router'; import { ErrorBoundary, Suspense } from 'solid-js'; +import { AuthProvider } from '~/lib/auth'; import './app.css'; export default function App() { @@ -10,19 +11,21 @@ export default function App() { root={(props) => ( NXTGAUGE Frontend Solid - ( -
-

Frontend Error

-

A runtime error occurred while rendering this page.

-
-                  {String((err as any)?.message || err)}
-                
-
- )} - > - {props.children} -
+ + ( +
+

Frontend Error

+

A runtime error occurred while rendering this page.

+
+                    {String((err as any)?.message || err)}
+                  
+
+ )} + > + {props.children} +
+
)} > diff --git a/src/components/admin/DashboardDesignPreview.tsx b/src/components/admin/DashboardDesignPreview.tsx index 04b7e5f..e2437cb 100644 --- a/src/components/admin/DashboardDesignPreview.tsx +++ b/src/components/admin/DashboardDesignPreview.tsx @@ -1686,9 +1686,21 @@ export default function DashboardDesignPreview(props: { } }; - const submitProfileForApproval = () => { - setProfileApprovalState('SUBMITTED'); - setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250); + const submitProfileForApproval = async () => { + if (!hasLive() || !livePrefix()) return; + try { + const res = await fetch(`${GW}/api/${livePrefix()}/profile/submit`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + }); + if (res.ok) { + setProfileApprovalState('SUBMITTED'); + setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250); + } + } catch { + setProfileApprovalState('SUBMITTED'); + } }; const submitPortfolioForApproval = () => { setPortfolioApprovalState('SUBMITTED'); @@ -3407,11 +3419,19 @@ export default function DashboardDesignPreview(props: { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ display_name: display || undefined, + full_name: display || undefined, + first_name: fn || undefined, + last_name: ln || undefined, + business_name: data['Business Name'] || undefined, + company_name: data['Company Name'] || undefined, + contact_person: data['Contact Person Name'] || undefined, bio: data['Bio'] || undefined, location: data['City'] || undefined, area: data['Area'] || undefined, state: data['State'] || undefined, pin_code: data['PIN Code'] || undefined, + phone: data['Mobile Number'] || undefined, + email: data['Email Address'] || undefined, }), }).then((r) => { setProfileSaving(false); diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..96f4012 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,180 @@ +const API = '/api/gateway'; + +function getAuthHeaders(): Record { + const token = typeof window !== 'undefined' + ? (sessionStorage.getItem('nxtgauge_access_token') || localStorage.getItem('nxtgauge_access_token') || '') + : ''; + return { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + +async function apiFetch(path: string, options?: RequestInit): Promise { + const res = await fetch(`${API}${path}`, { + headers: getAuthHeaders(), + credentials: 'include', + ...options, + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`API error ${res.status}: ${text}`); + } + return res.json(); +} + +export async function fetchProfile(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/profile/me`); +} + +export async function saveProfile(rolePrefix: string, payload: any): Promise { + return apiFetch(`/api/${rolePrefix}/profile/me`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); +} + +export async function submitProfileForVerification(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/profile/submit`, { + method: 'POST', + }); +} + +export async function fetchPortfolio(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/portfolio`); +} + +export async function createPortfolioItem(rolePrefix: string, payload: any): Promise { + return apiFetch(`/api/${rolePrefix}/portfolio`, { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export async function updatePortfolioItem(rolePrefix: string, id: string, payload: any): Promise { + return apiFetch(`/api/${rolePrefix}/portfolio/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); +} + +export async function deletePortfolioItem(rolePrefix: string, id: string): Promise { + return apiFetch(`/api/${rolePrefix}/portfolio/${id}`, { + method: 'DELETE', + }); +} + +export async function fetchServices(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/services`); +} + +export async function createService(rolePrefix: string, payload: any): Promise { + return apiFetch(`/api/${rolePrefix}/services`, { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export async function deleteService(rolePrefix: string, id: string): Promise { + return apiFetch(`/api/${rolePrefix}/services/${id}`, { + method: 'DELETE', + }); +} + +export async function fetchWallet(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/wallet`); +} + +export async function fetchLedger(rolePrefix: string, page = 1, limit = 20): Promise { + return apiFetch(`/api/${rolePrefix}/wallet/ledger?page=${page}&limit=${limit}`); +} + +export async function fetchInvoices(rolePrefix: string, page = 1, limit = 20): Promise { + return apiFetch(`/api/${rolePrefix}/wallet/invoices?page=${page}&limit=${limit}`); +} + +export async function createPaymentOrder(amount: number, packageId?: string): Promise { + return apiFetch('/api/payments/create-order', { + method: 'POST', + body: JSON.stringify({ amount, package_id: packageId }), + }); +} + +export async function verifyPayment(orderId: string, paymentId: string): Promise { + return apiFetch('/api/payments/verify', { + method: 'POST', + body: JSON.stringify({ order_id: orderId, payment_id: paymentId }), + }); +} + +export async function fetchPaymentStatus(paymentId: string): Promise { + return apiFetch(`/api/payments/${paymentId}/status`); +} + +export async function fetchJobs(rolePrefix: string, params?: Record): Promise { + const qs = params ? '?' + new URLSearchParams(params).toString() : ''; + return apiFetch(`/api/${rolePrefix}/jobs${qs}`); +} + +export async function applyToJob(rolePrefix: string, jobId: string, payload: any): Promise { + return apiFetch(`/api/${rolePrefix}/jobs/${jobId}/apply`, { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export async function fetchMyApplications(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/applications`); +} + +export async function fetchRequirements(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/requirements`); +} + +export async function createRequirement(rolePrefix: string, payload: any): Promise { + return apiFetch(`/api/${rolePrefix}/requirements`, { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export async function fetchMarketplace(rolePrefix: string, page = 1, limit = 20): Promise { + return apiFetch(`/api/${rolePrefix}/marketplace?page=${page}&limit=${limit}`); +} + +export async function createLeadRequest(rolePrefix: string, payload: any): Promise { + return apiFetch(`/api/${rolePrefix}/leads/request`, { + method: 'POST', + body: JSON.stringify(payload), + }); +} + +export async function fetchMyLeadRequests(rolePrefix: string): Promise { + return apiFetch(`/api/${rolePrefix}/leads/requests`); +} + +export async function uploadDocument(rolePrefix: string, file: File, documentType: string): Promise { + const formData = new FormData(); + formData.append('file', file); + formData.append('document_type', documentType); + + const token = typeof window !== 'undefined' + ? (sessionStorage.getItem('nxtgauge_access_token') || localStorage.getItem('nxtgauge_access_token') || '') + : ''; + + const res = await fetch(`${API}/api/${rolePrefix}/profile/documents`, { + method: 'POST', + headers: { + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + credentials: 'include', + body: formData, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`Upload error ${res.status}: ${text}`); + } + return res.json(); +} diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx new file mode 100644 index 0000000..09a94dc --- /dev/null +++ b/src/lib/auth.tsx @@ -0,0 +1,116 @@ +import { createContext, createResource, createSignal, useContext, type ParentProps, type Accessor, type Setter } from 'solid-js'; +import { useNavigate } from '@solidjs/router'; + +const API = '/api/gateway'; + +type AuthUser = { + id: string; + email: string; + full_name: string; + active_role: string; + email_verified: boolean; +}; + +type AuthState = { + user: Accessor; + isAuthenticated: Accessor; + isLoading: Accessor; + logout: () => void; + setUser: Setter; +}; + +const AuthContext = createContext(); + +function getToken(): string | null { + if (typeof window === 'undefined') return null; + return sessionStorage.getItem('nxtgauge_access_token') + || localStorage.getItem('nxtgauge_access_token') + || null; +} + +function clearAuthStorage() { + if (typeof window === 'undefined') return; + sessionStorage.removeItem('nxtgauge_access_token'); + sessionStorage.removeItem('nxtgauge_frontend_access_token'); + localStorage.removeItem('nxtgauge_access_token'); + localStorage.removeItem('nxtgauge_auth_user'); + localStorage.removeItem('nxtgauge_user'); + localStorage.removeItem('nxtgauge_signup_profile_v1'); +} + +async function fetchSession(): Promise { + const token = getToken(); + if (!token) return null; + try { + const res = await fetch(`${API}/api/auth/session`, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + if (!res.ok) return null; + const data = await res.json(); + if (!data?.id && !data?.user_id) return null; + return { + id: data.id || data.user_id, + email: data.email || '', + full_name: data.full_name || data.name || '', + active_role: data.active_role || data.role || 'JOB_SEEKER', + email_verified: data.email_verified || false, + }; + } catch { + return null; + } +} + +export function AuthProvider(props: ParentProps) { + const [user, setUser] = createSignal(null); + const [session] = createResource(fetchSession); + + const isLoading = () => session.loading; + const isAuthenticated = () => !!user() || (!!session() && !session.error); + + if (session()) { + setUser(session() as AuthUser | null); + } + + const logout = () => { + clearAuthStorage(); + setUser(null); + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + }; + + return ( + + {props.children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error('useAuth must be used within AuthProvider'); + return ctx; +} + +export function RequireAuth(props: ParentProps<{ fallback?: string }>) { + const navigate = useNavigate(); + const auth = useAuth(); + + if (auth.isLoading()) { + return
Loading...
; + } + + if (!auth.isAuthenticated()) { + const fallback = props.fallback || '/login'; + navigate(fallback, { replace: true }); + return null; + } + + return <>{props.children}; +} + +export { getToken, clearAuthStorage }; diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 242ce0d..e830dd5 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -1,4 +1,6 @@ import { Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js'; +import { useNavigate } from '@solidjs/router'; +import { useAuth, RequireAuth } from '~/lib/auth'; import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview'; type RoleKey = @@ -212,6 +214,8 @@ function mergeSidebar(role: RoleKey, runtimeSidebar: string[]): string[] { } export default function RuntimeDashboardPage() { + const navigate = useNavigate(); + const auth = useAuth(); const [hydrated, setHydrated] = createSignal(false); const [role, setRole] = createSignal('JOB_SEEKER'); const [activeSidebar, setActiveSidebar] = createSignal('My Dashboard'); @@ -221,20 +225,24 @@ export default function RuntimeDashboardPage() { onMount(() => { setHydrated(true); - setRole(getInitialRoleFromStorage()); + const storedRole = getInitialRoleFromStorage(); + setRole(storedRole); setUserName(getNameFromStorage()); - // Fetch fresh session data - fetchJson('/api/auth/session').then((data) => { - if (!data) return; - const name = data.full_name - || data.name - || (data.first_name ? `${data.first_name} ${data.last_name ?? ''}`.trim() : '') - || data.email?.split('@')[0] - || ''; - if (name) setUserName(name); - if (data.id || data.user_id) setUserId(String(data.id || data.user_id)); - if (data.active_role) setRole(normalizeRole(data.active_role)); - }); + if (auth.user()) { + const u = auth.user()!; + if (u.full_name) setUserName(u.full_name); + if (u.id) setUserId(u.id); + if (u.active_role) setRole(normalizeRole(u.active_role)); + } + }); + + createEffect(() => { + const u = auth.user(); + if (u) { + if (u.full_name && userName() === 'User') setUserName(u.full_name); + if (u.id && !userId()) setUserId(u.id); + if (u.active_role) setRole(normalizeRole(u.active_role)); + } }); const [bundle] = createResource( @@ -272,33 +280,35 @@ export default function RuntimeDashboardPage() { }); return ( -
-
+ +
+
- -
Loading dashboard...
-
+ +
Loading dashboard...
+
- - - -
-
+ + + +
+
+ ); } diff --git a/src/routes/forgot-password.tsx b/src/routes/forgot-password.tsx new file mode 100644 index 0000000..78a49e2 --- /dev/null +++ b/src/routes/forgot-password.tsx @@ -0,0 +1,194 @@ +import { A, useNavigate, useSearchParams } from '@solidjs/router'; +import { createMemo, createSignal, Show } from 'solid-js'; +import PublicBackground from '~/components/PublicBackground'; +import PublicHeader from '~/components/PublicHeader'; +import { isValidEmail } from '~/lib/form-validation'; + +function PasswordVisibilityIcon(props: { visible: boolean }) { + if (props.visible) { + return ( + + ); + } + return ( + + ); +} + +export default function ForgotPasswordRoute() { + const navigate = useNavigate(); + const [search] = useSearchParams(); + const [email, setEmail] = createSignal(''); + const [token, setToken] = createSignal(search.token || ''); + const [newPassword, setNewPassword] = createSignal(''); + const [confirmPassword, setConfirmPassword] = createSignal(''); + const [showPassword, setShowPassword] = createSignal(false); + const [showConfirm, setShowConfirm] = createSignal(false); + const [error, setError] = createSignal(''); + const [success, setSuccess] = createSignal(''); + const [submitting, setSubmitting] = createSignal(false); + const [step, setStep] = createSignal<'request' | 'reset'>(token() ? 'reset' : 'request'); + + const passwordsMatch = createMemo(() => newPassword() === confirmPassword() && newPassword().length >= 8); + + const requestReset = async () => { + setError(''); + setSuccess(''); + if (!isValidEmail(email())) { + setError('Enter a valid email address.'); + return; + } + setSubmitting(true); + try { + const res = await fetch('/api/gateway/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email().trim().toLowerCase() }), + }); + if (res.ok) { + setSuccess('If an account exists with this email, a reset link has been sent.'); + } else { + setError('Unable to process request. Please try again.'); + } + } catch { + setError('Network error. Please check your connection.'); + } finally { + setSubmitting(false); + } + }; + + const resetPassword = async () => { + setError(''); + setSuccess(''); + if (!token()) { + setError('Reset token is missing. Use the link from your email.'); + return; + } + if (newPassword().length < 8) { + setError('Password must be at least 8 characters.'); + return; + } + if (!passwordsMatch()) { + setError('Passwords do not match.'); + return; + } + setSubmitting(true); + try { + const res = await fetch('/api/gateway/api/auth/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token: token(), + new_password: newPassword(), + }), + }); + const data = await res.json().catch(() => ({})); + if (res.ok) { + setSuccess('Password reset successfully. You can now login.'); + setTimeout(() => navigate('/login', { replace: true }), 2000); + } else { + setError(String(data?.message || data?.error || 'Reset failed. Token may be expired.')); + } + } catch { + setError('Network error. Please check your connection.'); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ + + +
+
+ Reset Password +
+
+

Account Recovery

+

Reset Your Password

+

Securely reset your password with email verification.

+
+
+ +
+ +

Set New Password

+

Enter your new password below.

+ +
+ +
+ setNewPassword(e.currentTarget.value)} placeholder="Enter new password" /> + +
+

= 8 ? '#fd6116' : '#6e7591' }}> + {newPassword().length >= 8 ? '✓ Meets minimum length' : '• Minimum 8 characters required'} +

+
+ +
+ +
+ setConfirmPassword(e.currentTarget.value)} placeholder="Confirm new password" /> + +
+

+ {confirmPassword() && passwordsMatch() ? '✓ Passwords match' : '• Passwords do not match'} +

+
+ + + + + + }> +

Forgot Password

+

Enter your email to receive a password reset link.

+ +
+ + setEmail(e.currentTarget.value)} placeholder="Enter your registered email" /> +

+ {email().trim() && isValidEmail(email()) ? '✓ Valid email format' : '• Enter a valid email format'} +

+
+ + + + +
+ + +

{error()}

+
+ +

{success()}

+
+
+
+
+ ); +} diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 21f285f..01de512 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,5 +1,6 @@ import { A, useNavigate } from '@solidjs/router'; import { createMemo, createSignal, For, Show } from 'solid-js'; +import { useAuth } from '~/lib/auth'; import PublicBackground from '~/components/PublicBackground'; import PublicHeader from '~/components/PublicHeader'; import CaptchaCanvas from '~/components/CaptchaCanvas'; @@ -33,6 +34,7 @@ function PasswordVisibilityIcon(props: { visible: boolean }) { export default function LoginRoute() { const navigate = useNavigate(); + const auth = useAuth(); const [email, setEmail] = createSignal(''); const [password, setPassword] = createSignal(''); const [otp, setOtp] = createSignal(['', '', '', '', '', '']); @@ -91,10 +93,6 @@ export default function LoginRoute() { const login = async () => { setError(''); - if (email().trim().toLowerCase() === 'demo@nxtgauge.com' && password() === 'Demo@1234') { - dummyLogin(); - return; - } if (!isValidEmail(email())) { setError('Enter a valid email address.'); return; @@ -139,6 +137,15 @@ export default function LoginRoute() { window.localStorage.setItem('nxtgauge_access_token', accessToken); } saveUser(data?.user || {}); + if (auth.setUser) { + auth.setUser({ + id: data?.user?.id || '', + email: data?.user?.email || email().trim().toLowerCase(), + full_name: data?.user?.full_name || '', + active_role: data?.user?.active_role || 'JOB_SEEKER', + email_verified: data?.user?.email_verified || false, + }); + } navigate('/dashboard', { replace: true }); } finally { setSubmitting(false); @@ -189,38 +196,6 @@ export default function LoginRoute() { } }; - const dummyLogin = () => { - const demoUser = { - id: 'demo-user-001', - email: 'demo@nxtgauge.com', - full_name: 'Demo User', - email_verified: true, - active_role: 'JOB_SEEKER', - roles: ['JOB_SEEKER'], - }; - const payload = { - firstName: 'Demo', - lastName: 'User', - fullName: 'Demo User', - name: 'Demo User', - displayName: 'Demo User', - email: 'demo@nxtgauge.com', - roleKey: 'job_seeker', - role: 'job_seeker', - user: demoUser, - }; - if (typeof window !== 'undefined') { - window.sessionStorage.setItem('nxtgauge_access_token', 'dummy-access-token'); - window.sessionStorage.setItem('nxtgauge_frontend_access_token', 'dummy-access-token'); - window.localStorage.setItem('nxtgauge_access_token', 'dummy-access-token'); - window.localStorage.setItem('nxtgauge_auth_user', JSON.stringify(payload)); - window.localStorage.setItem('nxtgauge_user', JSON.stringify(payload)); - window.localStorage.setItem('nxtgauge_signup_profile_v1', JSON.stringify(payload)); - } - setRoleGuess('job_seeker'); - navigate('/dashboard', { replace: true }); - }; - return (
@@ -300,10 +275,6 @@ export default function LoginRoute() { {submitting() ? 'Signing In...' : 'Sign In'} - -