feat: add auth context, route guards, password reset, and API client

- Add AuthProvider context and RequireAuth route guard
- Create API client with all endpoint helpers
- Add forgot-password route wired to backend reset endpoints
- Remove dummy login button from login page
- Wire dashboard to auth context for user data
- Enhance profile save to send all fields
- Wire profile submit-for-verification to backend API
This commit is contained in:
Ashwin Kumar 2026-04-06 06:19:23 +02:00
parent 19a0850c49
commit e5406a0061
7 changed files with 589 additions and 94 deletions

View file

@ -2,6 +2,7 @@ import { MetaProvider, Title } from '@solidjs/meta';
import { Router } from '@solidjs/router'; import { Router } from '@solidjs/router';
import { FileRoutes } from '@solidjs/start/router'; import { FileRoutes } from '@solidjs/start/router';
import { ErrorBoundary, Suspense } from 'solid-js'; import { ErrorBoundary, Suspense } from 'solid-js';
import { AuthProvider } from '~/lib/auth';
import './app.css'; import './app.css';
export default function App() { export default function App() {
@ -10,19 +11,21 @@ export default function App() {
root={(props) => ( root={(props) => (
<MetaProvider> <MetaProvider>
<Title>NXTGAUGE Frontend Solid</Title> <Title>NXTGAUGE Frontend Solid</Title>
<ErrorBoundary <AuthProvider>
fallback={(err) => ( <ErrorBoundary
<main style={{ padding: '24px', 'font-family': 'Inter, system-ui, sans-serif', color: '#111827', background: '#fff' }}> fallback={(err) => (
<h1 style={{ margin: 0, 'font-size': '20px' }}>Frontend Error</h1> <main style={{ padding: '24px', 'font-family': 'Inter, system-ui, sans-serif', color: '#111827', background: '#fff' }}>
<p style={{ 'margin-top': '8px' }}>A runtime error occurred while rendering this page.</p> <h1 style={{ margin: 0, 'font-size': '20px' }}>Frontend Error</h1>
<pre style={{ 'margin-top': '12px', padding: '12px', background: '#f3f4f6', 'border-radius': '8px', 'white-space': 'pre-wrap' }}> <p style={{ 'margin-top': '8px' }}>A runtime error occurred while rendering this page.</p>
{String((err as any)?.message || err)} <pre style={{ 'margin-top': '12px', padding: '12px', background: '#f3f4f6', 'border-radius': '8px', 'white-space': 'pre-wrap' }}>
</pre> {String((err as any)?.message || err)}
</main> </pre>
)} </main>
> )}
<Suspense>{props.children}</Suspense> >
</ErrorBoundary> <Suspense>{props.children}</Suspense>
</ErrorBoundary>
</AuthProvider>
</MetaProvider> </MetaProvider>
)} )}
> >

View file

@ -1686,9 +1686,21 @@ export default function DashboardDesignPreview(props: {
} }
}; };
const submitProfileForApproval = () => { const submitProfileForApproval = async () => {
setProfileApprovalState('SUBMITTED'); if (!hasLive() || !livePrefix()) return;
setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250); 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 = () => { const submitPortfolioForApproval = () => {
setPortfolioApprovalState('SUBMITTED'); setPortfolioApprovalState('SUBMITTED');
@ -3407,11 +3419,19 @@ export default function DashboardDesignPreview(props: {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
display_name: display || undefined, 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, bio: data['Bio'] || undefined,
location: data['City'] || undefined, location: data['City'] || undefined,
area: data['Area'] || undefined, area: data['Area'] || undefined,
state: data['State'] || undefined, state: data['State'] || undefined,
pin_code: data['PIN Code'] || undefined, pin_code: data['PIN Code'] || undefined,
phone: data['Mobile Number'] || undefined,
email: data['Email Address'] || undefined,
}), }),
}).then((r) => { }).then((r) => {
setProfileSaving(false); setProfileSaving(false);

180
src/lib/api.ts Normal file
View file

@ -0,0 +1,180 @@
const API = '/api/gateway';
function getAuthHeaders(): Record<string, string> {
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<any> {
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<any> {
return apiFetch(`/api/${rolePrefix}/profile/me`);
}
export async function saveProfile(rolePrefix: string, payload: any): Promise<any> {
return apiFetch(`/api/${rolePrefix}/profile/me`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
export async function submitProfileForVerification(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/profile/submit`, {
method: 'POST',
});
}
export async function fetchPortfolio(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/portfolio`);
}
export async function createPortfolioItem(rolePrefix: string, payload: any): Promise<any> {
return apiFetch(`/api/${rolePrefix}/portfolio`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function updatePortfolioItem(rolePrefix: string, id: string, payload: any): Promise<any> {
return apiFetch(`/api/${rolePrefix}/portfolio/${id}`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
export async function deletePortfolioItem(rolePrefix: string, id: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/portfolio/${id}`, {
method: 'DELETE',
});
}
export async function fetchServices(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/services`);
}
export async function createService(rolePrefix: string, payload: any): Promise<any> {
return apiFetch(`/api/${rolePrefix}/services`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function deleteService(rolePrefix: string, id: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/services/${id}`, {
method: 'DELETE',
});
}
export async function fetchWallet(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/wallet`);
}
export async function fetchLedger(rolePrefix: string, page = 1, limit = 20): Promise<any> {
return apiFetch(`/api/${rolePrefix}/wallet/ledger?page=${page}&limit=${limit}`);
}
export async function fetchInvoices(rolePrefix: string, page = 1, limit = 20): Promise<any> {
return apiFetch(`/api/${rolePrefix}/wallet/invoices?page=${page}&limit=${limit}`);
}
export async function createPaymentOrder(amount: number, packageId?: string): Promise<any> {
return apiFetch('/api/payments/create-order', {
method: 'POST',
body: JSON.stringify({ amount, package_id: packageId }),
});
}
export async function verifyPayment(orderId: string, paymentId: string): Promise<any> {
return apiFetch('/api/payments/verify', {
method: 'POST',
body: JSON.stringify({ order_id: orderId, payment_id: paymentId }),
});
}
export async function fetchPaymentStatus(paymentId: string): Promise<any> {
return apiFetch(`/api/payments/${paymentId}/status`);
}
export async function fetchJobs(rolePrefix: string, params?: Record<string, string>): Promise<any> {
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
return apiFetch(`/api/${rolePrefix}/jobs${qs}`);
}
export async function applyToJob(rolePrefix: string, jobId: string, payload: any): Promise<any> {
return apiFetch(`/api/${rolePrefix}/jobs/${jobId}/apply`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function fetchMyApplications(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/applications`);
}
export async function fetchRequirements(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/requirements`);
}
export async function createRequirement(rolePrefix: string, payload: any): Promise<any> {
return apiFetch(`/api/${rolePrefix}/requirements`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function fetchMarketplace(rolePrefix: string, page = 1, limit = 20): Promise<any> {
return apiFetch(`/api/${rolePrefix}/marketplace?page=${page}&limit=${limit}`);
}
export async function createLeadRequest(rolePrefix: string, payload: any): Promise<any> {
return apiFetch(`/api/${rolePrefix}/leads/request`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function fetchMyLeadRequests(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/leads/requests`);
}
export async function uploadDocument(rolePrefix: string, file: File, documentType: string): Promise<any> {
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();
}

116
src/lib/auth.tsx Normal file
View file

@ -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<AuthUser | null>;
isAuthenticated: Accessor<boolean>;
isLoading: Accessor<boolean>;
logout: () => void;
setUser: Setter<AuthUser | null>;
};
const AuthContext = createContext<AuthState>();
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<AuthUser | null> {
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<AuthUser | null>(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 (
<AuthContext.Provider value={{ user, isAuthenticated, isLoading, logout, setUser }}>
{props.children}
</AuthContext.Provider>
);
}
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 <div class="flex min-h-screen items-center justify-center text-[#6B7280]">Loading...</div>;
}
if (!auth.isAuthenticated()) {
const fallback = props.fallback || '/login';
navigate(fallback, { replace: true });
return null;
}
return <>{props.children}</>;
}
export { getToken, clearAuthStorage };

View file

@ -1,4 +1,6 @@
import { Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js'; 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'; import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview';
type RoleKey = type RoleKey =
@ -212,6 +214,8 @@ function mergeSidebar(role: RoleKey, runtimeSidebar: string[]): string[] {
} }
export default function RuntimeDashboardPage() { export default function RuntimeDashboardPage() {
const navigate = useNavigate();
const auth = useAuth();
const [hydrated, setHydrated] = createSignal(false); const [hydrated, setHydrated] = createSignal(false);
const [role, setRole] = createSignal<RoleKey>('JOB_SEEKER'); const [role, setRole] = createSignal<RoleKey>('JOB_SEEKER');
const [activeSidebar, setActiveSidebar] = createSignal('My Dashboard'); const [activeSidebar, setActiveSidebar] = createSignal('My Dashboard');
@ -221,20 +225,24 @@ export default function RuntimeDashboardPage() {
onMount(() => { onMount(() => {
setHydrated(true); setHydrated(true);
setRole(getInitialRoleFromStorage()); const storedRole = getInitialRoleFromStorage();
setRole(storedRole);
setUserName(getNameFromStorage()); setUserName(getNameFromStorage());
// Fetch fresh session data if (auth.user()) {
fetchJson('/api/auth/session').then((data) => { const u = auth.user()!;
if (!data) return; if (u.full_name) setUserName(u.full_name);
const name = data.full_name if (u.id) setUserId(u.id);
|| data.name if (u.active_role) setRole(normalizeRole(u.active_role));
|| (data.first_name ? `${data.first_name} ${data.last_name ?? ''}`.trim() : '') }
|| data.email?.split('@')[0] });
|| '';
if (name) setUserName(name); createEffect(() => {
if (data.id || data.user_id) setUserId(String(data.id || data.user_id)); const u = auth.user();
if (data.active_role) setRole(normalizeRole(data.active_role)); 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( const [bundle] = createResource(
@ -272,33 +280,35 @@ export default function RuntimeDashboardPage() {
}); });
return ( return (
<main style={{ 'min-height': '100vh', background: '#f3f4f6' }}> <RequireAuth>
<div> <main style={{ 'min-height': '100vh', background: '#f3f4f6' }}>
<div>
<Show when={loading()}> <Show when={loading()}>
<div style={cardStyle}>Loading dashboard...</div> <div style={cardStyle}>Loading dashboard...</div>
</Show> </Show>
<Show when={ready()}> <Show when={ready()}>
<DashboardDesignPreview <DashboardDesignPreview
status={bundle()?.status ?? 'ACTIVE'} status={bundle()?.status ?? 'ACTIVE'}
sidebarItems={sidebarItems()} sidebarItems={sidebarItems()}
activeSidebar={activeSidebar()} activeSidebar={activeSidebar()}
onSidebarSelect={setActiveSidebar} onSidebarSelect={setActiveSidebar}
tabs={tabs()} tabs={tabs()}
activeTab={activeTab()} activeTab={activeTab()}
onTabSelect={setActiveTab} onTabSelect={setActiveTab}
widgets={bundle()?.widgets || []} widgets={bundle()?.widgets || []}
fields={bundle()?.fields || []} fields={bundle()?.fields || []}
mode="customer_external" mode="customer_external"
roleKey={role()} roleKey={role()}
exploreRoles={EXPLORE_ROLES} exploreRoles={EXPLORE_ROLES}
hidePreviewHeader hidePreviewHeader
liveData={liveData()} liveData={liveData()}
/> />
</Show> </Show>
</div> </div>
</main> </main>
</RequireAuth>
); );
} }

View file

@ -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 (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 3l18 18" />
<path d="M10.58 10.58a2 2 0 0 0 2.83 2.83" />
<path d="M9.88 5.09A11 11 0 0 1 12 4.9c5.5 0 10 4.1 10 7.1 0 1.2-.72 2.53-1.95 3.72" />
<path d="M6.1 6.1C3.54 7.58 2 9.79 2 12c0 3 4.48 7.1 10 7.1 1.72 0 3.36-.4 4.84-1.12" />
</svg>
);
}
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
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 (
<main class="auth-page">
<PublicBackground />
<PublicHeader />
<div class="auth-layout">
<section class="auth-visual card glass-dark">
<img class="auth-visual-img" src="/images/auth-company-1.jpg" alt="Reset Password" />
<div class="auth-visual-overlay" />
<div class="auth-visual-content">
<p class="eyebrow">Account Recovery</p>
<h1 class="title light">Reset Your Password</h1>
<p class="subtitle light">Securely reset your password with email verification.</p>
</div>
</section>
<section class="auth-form card glass-light">
<Show when={step() === 'request'} fallback={
<>
<h2 class="title">Set New Password</h2>
<p class="subtitle">Enter your new password below.</p>
<div class="field">
<label class="label" for="new-password">NEW PASSWORD</label>
<div class="auth-password-wrap">
<input id="new-password" type={showPassword() ? 'text' : 'password'} class="input" value={newPassword()} onInput={(e) => setNewPassword(e.currentTarget.value)} placeholder="Enter new password" />
<button class="auth-toggle-visibility" type="button" onClick={() => setShowPassword((p) => !p)} aria-label={showPassword() ? 'Hide password' : 'Show password'}>
<PasswordVisibilityIcon visible={showPassword()} />
</button>
</div>
<p class="validation-note" style={{ color: newPassword().length >= 8 ? '#fd6116' : '#6e7591' }}>
{newPassword().length >= 8 ? '✓ Meets minimum length' : '• Minimum 8 characters required'}
</p>
</div>
<div class="field">
<label class="label" for="confirm-password">CONFIRM PASSWORD</label>
<div class="auth-password-wrap">
<input id="confirm-password" type={showConfirm() ? 'text' : 'password'} class="input" value={confirmPassword()} onInput={(e) => setConfirmPassword(e.currentTarget.value)} placeholder="Confirm new password" />
<button class="auth-toggle-visibility" type="button" onClick={() => setShowConfirm((p) => !p)} aria-label={showConfirm() ? 'Hide password' : 'Show password'}>
<PasswordVisibilityIcon visible={showConfirm()} />
</button>
</div>
<p class="validation-note" style={{ color: passwordsMatch() ? '#fd6116' : '#6e7591' }}>
{confirmPassword() && passwordsMatch() ? '✓ Passwords match' : '• Passwords do not match'}
</p>
</div>
<button class="auth-submit-btn" type="button" onClick={() => void resetPassword()} disabled={submitting()}>
{submitting() ? 'Resetting...' : 'Reset Password'}
</button>
<div class="auth-footer-row">
<p class="note"><A href="/login">Back to Sign In</A></p>
</div>
</>
}>
<h2 class="title">Forgot Password</h2>
<p class="subtitle">Enter your email to receive a password reset link.</p>
<div class="field">
<label class="label" for="reset-email">EMAIL ADDRESS</label>
<input id="reset-email" type="email" class="input" value={email()} onInput={(e) => setEmail(e.currentTarget.value)} placeholder="Enter your registered email" />
<p class="validation-note" style={{ color: email().trim() && isValidEmail(email()) ? '#fd6116' : '#6e7591' }}>
{email().trim() && isValidEmail(email()) ? '✓ Valid email format' : '• Enter a valid email format'}
</p>
</div>
<button class="auth-submit-btn" type="button" onClick={() => void requestReset()} disabled={submitting()}>
{submitting() ? 'Sending...' : 'Send Reset Link'}
</button>
<div class="auth-footer-row">
<p class="note"><A href="/login">Back to Sign In</A></p>
</div>
</Show>
<Show when={error()}>
<p class="error">{error()}</p>
</Show>
<Show when={success()}>
<p class="error" style={{ color: '#16A34A' }}>{success()}</p>
</Show>
</section>
</div>
</main>
);
}

View file

@ -1,5 +1,6 @@
import { A, useNavigate } from '@solidjs/router'; import { A, useNavigate } from '@solidjs/router';
import { createMemo, createSignal, For, Show } from 'solid-js'; import { createMemo, createSignal, For, Show } from 'solid-js';
import { useAuth } from '~/lib/auth';
import PublicBackground from '~/components/PublicBackground'; import PublicBackground from '~/components/PublicBackground';
import PublicHeader from '~/components/PublicHeader'; import PublicHeader from '~/components/PublicHeader';
import CaptchaCanvas from '~/components/CaptchaCanvas'; import CaptchaCanvas from '~/components/CaptchaCanvas';
@ -33,6 +34,7 @@ function PasswordVisibilityIcon(props: { visible: boolean }) {
export default function LoginRoute() { export default function LoginRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
const auth = useAuth();
const [email, setEmail] = createSignal(''); const [email, setEmail] = createSignal('');
const [password, setPassword] = createSignal(''); const [password, setPassword] = createSignal('');
const [otp, setOtp] = createSignal(['', '', '', '', '', '']); const [otp, setOtp] = createSignal(['', '', '', '', '', '']);
@ -91,10 +93,6 @@ export default function LoginRoute() {
const login = async () => { const login = async () => {
setError(''); setError('');
if (email().trim().toLowerCase() === 'demo@nxtgauge.com' && password() === 'Demo@1234') {
dummyLogin();
return;
}
if (!isValidEmail(email())) { if (!isValidEmail(email())) {
setError('Enter a valid email address.'); setError('Enter a valid email address.');
return; return;
@ -139,6 +137,15 @@ export default function LoginRoute() {
window.localStorage.setItem('nxtgauge_access_token', accessToken); window.localStorage.setItem('nxtgauge_access_token', accessToken);
} }
saveUser(data?.user || {}); 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 }); navigate('/dashboard', { replace: true });
} finally { } finally {
setSubmitting(false); 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 ( return (
<main class="auth-page"> <main class="auth-page">
<PublicBackground /> <PublicBackground />
@ -300,10 +275,6 @@ export default function LoginRoute() {
{submitting() ? 'Signing In...' : 'Sign In'} {submitting() ? 'Signing In...' : 'Sign In'}
</button> </button>
<button class="auth-submit-btn" type="button" onClick={dummyLogin} disabled={submitting()} style={{ 'margin-top': '8px', background: '#111827' }}>
Dummy Login (Dashboard Test)
</button>
<Show when={showVerify()}> <Show when={showVerify()}>
<button class="auth-submit-btn" type="button" onClick={() => void verifyThenLogin()} disabled={submitting()}> <button class="auth-submit-btn" type="button" onClick={() => void verifyThenLogin()} disabled={submitting()}>
{submitting() ? 'Verifying...' : 'Verify Email and Login'} {submitting() ? 'Verifying...' : 'Verify Email and Login'}
@ -313,6 +284,7 @@ export default function LoginRoute() {
<div class="auth-footer-row"> <div class="auth-footer-row">
<p class="footer-text">Secure login with email verification.</p> <p class="footer-text">Secure login with email verification.</p>
<p class="note">New user? <A href="/signup">Sign Up</A></p> <p class="note">New user? <A href="/signup">Sign Up</A></p>
<p class="note"><A href="/forgot-password">Forgot Password?</A></p>
</div> </div>
<Show when={error()}> <Show when={error()}>