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:
parent
19a0850c49
commit
e5406a0061
7 changed files with 589 additions and 94 deletions
|
|
@ -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,6 +11,7 @@ export default function App() {
|
|||
root={(props) => (
|
||||
<MetaProvider>
|
||||
<Title>NXTGAUGE Frontend Solid</Title>
|
||||
<AuthProvider>
|
||||
<ErrorBoundary
|
||||
fallback={(err) => (
|
||||
<main style={{ padding: '24px', 'font-family': 'Inter, system-ui, sans-serif', color: '#111827', background: '#fff' }}>
|
||||
|
|
@ -23,6 +25,7 @@ export default function App() {
|
|||
>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</ErrorBoundary>
|
||||
</AuthProvider>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1686,9 +1686,21 @@ export default function DashboardDesignPreview(props: {
|
|||
}
|
||||
};
|
||||
|
||||
const submitProfileForApproval = () => {
|
||||
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);
|
||||
|
|
|
|||
180
src/lib/api.ts
Normal file
180
src/lib/api.ts
Normal 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
116
src/lib/auth.tsx
Normal 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 };
|
||||
|
|
@ -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<RoleKey>('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,6 +280,7 @@ export default function RuntimeDashboardPage() {
|
|||
});
|
||||
|
||||
return (
|
||||
<RequireAuth>
|
||||
<main style={{ 'min-height': '100vh', background: '#f3f4f6' }}>
|
||||
<div>
|
||||
|
||||
|
|
@ -299,6 +308,7 @@ export default function RuntimeDashboardPage() {
|
|||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
</RequireAuth>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
194
src/routes/forgot-password.tsx
Normal file
194
src/routes/forgot-password.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<main class="auth-page">
|
||||
<PublicBackground />
|
||||
|
|
@ -300,10 +275,6 @@ export default function LoginRoute() {
|
|||
{submitting() ? 'Signing In...' : 'Sign In'}
|
||||
</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()}>
|
||||
<button class="auth-submit-btn" type="button" onClick={() => void verifyThenLogin()} disabled={submitting()}>
|
||||
{submitting() ? 'Verifying...' : 'Verify Email and Login'}
|
||||
|
|
@ -313,6 +284,7 @@ export default function LoginRoute() {
|
|||
<div class="auth-footer-row">
|
||||
<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"><A href="/forgot-password">Forgot Password?</A></p>
|
||||
</div>
|
||||
|
||||
<Show when={error()}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue