fix(auth): correct resend-otp API endpoint path
- Change from /api/gateway/api/auth/resend-otp to /api/auth/resend-otp - Fix in signup.tsx and login.tsx - Gateway already proxies /api/auth/* to users service
This commit is contained in:
parent
a365e1fa94
commit
152f918a7b
2 changed files with 166 additions and 109 deletions
|
|
@ -1,27 +1,32 @@
|
||||||
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 { 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";
|
||||||
import { isValidEmail } from '~/lib/form-validation';
|
import { isValidEmail } from "~/lib/form-validation";
|
||||||
|
|
||||||
type RoleKey = 'company' | 'job_seeker' | 'professional' | 'customer';
|
type RoleKey = "company" | "job_seeker" | "professional" | "customer";
|
||||||
|
|
||||||
function normalizeRoleValue(value: unknown): string {
|
function normalizeRoleValue(value: unknown): string {
|
||||||
return String(value || '').trim().toUpperCase().replace(/\s+/g, '_');
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/\s+/g, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredPreferredRole(emailHint?: string): string | null {
|
function getStoredPreferredRole(emailHint?: string): string | null {
|
||||||
if (typeof window === 'undefined') return null;
|
if (typeof window === "undefined") return null;
|
||||||
const keys = ['nxtgauge_signup_profile_v1', 'nxtgauge_auth_user', 'nxtgauge_user'];
|
const keys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"];
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const raw = window.localStorage.getItem(key);
|
const raw = window.localStorage.getItem(key);
|
||||||
if (!raw) continue;
|
if (!raw) continue;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw) as Record<string, any>;
|
const parsed = JSON.parse(raw) as Record<string, any>;
|
||||||
const storedEmail = String(parsed?.email || '').trim().toLowerCase();
|
const storedEmail = String(parsed?.email || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
if (emailHint && storedEmail && storedEmail !== emailHint.trim().toLowerCase()) continue;
|
if (emailHint && storedEmail && storedEmail !== emailHint.trim().toLowerCase()) continue;
|
||||||
|
|
||||||
const selectedProfessionalRole = normalizeRoleValue(parsed?.selectedProfessionalRole);
|
const selectedProfessionalRole = normalizeRoleValue(parsed?.selectedProfessionalRole);
|
||||||
|
|
@ -42,12 +47,12 @@ function resolveActiveRole(rawBackendRole: unknown, emailHint?: string): string
|
||||||
if (backendRole) return backendRole;
|
if (backendRole) return backendRole;
|
||||||
const preferredRole = getStoredPreferredRole(emailHint);
|
const preferredRole = getStoredPreferredRole(emailHint);
|
||||||
if (preferredRole) return preferredRole;
|
if (preferredRole) return preferredRole;
|
||||||
return preferredRole || 'JOB_SEEKER';
|
return preferredRole || "JOB_SEEKER";
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCaptcha() {
|
function makeCaptcha() {
|
||||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function PasswordVisibilityIcon(props: { visible: boolean }) {
|
function PasswordVisibilityIcon(props: { visible: boolean }) {
|
||||||
|
|
@ -72,45 +77,45 @@ function PasswordVisibilityIcon(props: { visible: boolean }) {
|
||||||
export default function LoginRoute() {
|
export default function LoginRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const auth = useAuth();
|
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(["", "", "", "", "", ""]);
|
||||||
const [showVerify, setShowVerify] = createSignal(false);
|
const [showVerify, setShowVerify] = createSignal(false);
|
||||||
const [showPassword, setShowPassword] = createSignal(false);
|
const [showPassword, setShowPassword] = createSignal(false);
|
||||||
const [captcha, setCaptcha] = createSignal(makeCaptcha());
|
const [captcha, setCaptcha] = createSignal(makeCaptcha());
|
||||||
const [captchaInput, setCaptchaInput] = createSignal('');
|
const [captchaInput, setCaptchaInput] = createSignal("");
|
||||||
const [error, setError] = createSignal('');
|
const [error, setError] = createSignal("");
|
||||||
const [submitting, setSubmitting] = createSignal(false);
|
const [submitting, setSubmitting] = createSignal(false);
|
||||||
const [roleGuess, setRoleGuess] = createSignal<RoleKey>('job_seeker');
|
const [roleGuess, setRoleGuess] = createSignal<RoleKey>("job_seeker");
|
||||||
const [roleHint, setRoleHint] = createSignal('');
|
const [roleHint, setRoleHint] = createSignal("");
|
||||||
const [checkingRole, setCheckingRole] = createSignal(false);
|
const [checkingRole, setCheckingRole] = createSignal(false);
|
||||||
|
|
||||||
const otpCode = createMemo(() => otp().join(''));
|
const otpCode = createMemo(() => otp().join(""));
|
||||||
|
|
||||||
const formatRoleLabel = (value: string): string =>
|
const formatRoleLabel = (value: string): string =>
|
||||||
String(value || '')
|
String(value || "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/[_\s]+/g, ' ')
|
.replace(/[_\s]+/g, " ")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
||||||
|
|
||||||
const lookupRoleByEmail = async (emailValue: string) => {
|
const lookupRoleByEmail = async (emailValue: string) => {
|
||||||
const normalized = emailValue.trim().toLowerCase();
|
const normalized = emailValue.trim().toLowerCase();
|
||||||
if (!normalized || !isValidEmail(normalized)) {
|
if (!normalized || !isValidEmail(normalized)) {
|
||||||
setRoleHint('');
|
setRoleHint("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCheckingRole(true);
|
setCheckingRole(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/gateway/api/auth/check-email', {
|
const response = await fetch("/api/gateway/api/auth/check-email", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
body: JSON.stringify({ email: normalized }),
|
body: JSON.stringify({ email: normalized }),
|
||||||
});
|
});
|
||||||
const payload = await response.json().catch(() => ({}));
|
const payload = await response.json().catch(() => ({}));
|
||||||
if (!response.ok || !payload?.exists) {
|
if (!response.ok || !payload?.exists) {
|
||||||
setRoleHint('');
|
setRoleHint("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const detectedRole = normalizeRoleValue(
|
const detectedRole = normalizeRoleValue(
|
||||||
|
|
@ -118,27 +123,28 @@ export default function LoginRoute() {
|
||||||
);
|
);
|
||||||
if (!detectedRole) {
|
if (!detectedRole) {
|
||||||
const fallbackRole = normalizeRoleValue(getStoredPreferredRole(normalized));
|
const fallbackRole = normalizeRoleValue(getStoredPreferredRole(normalized));
|
||||||
setRoleHint(
|
setRoleHint(fallbackRole ? `Role: ${formatRoleLabel(fallbackRole)}` : "Role: Not assigned");
|
||||||
fallbackRole
|
|
||||||
? `Role: ${formatRoleLabel(fallbackRole)}`
|
|
||||||
: 'Role: Not assigned'
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRoleHint(`Role: ${formatRoleLabel(detectedRole)}`);
|
setRoleHint(`Role: ${formatRoleLabel(detectedRole)}`);
|
||||||
const roleLower = detectedRole.toLowerCase();
|
const roleLower = detectedRole.toLowerCase();
|
||||||
if (roleLower === 'company' || roleLower === 'customer' || roleLower === 'job_seeker' || roleLower === 'professional') {
|
if (
|
||||||
|
roleLower === "company" ||
|
||||||
|
roleLower === "customer" ||
|
||||||
|
roleLower === "job_seeker" ||
|
||||||
|
roleLower === "professional"
|
||||||
|
) {
|
||||||
setRoleGuess(roleLower as RoleKey);
|
setRoleGuess(roleLower as RoleKey);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setRoleHint('');
|
setRoleHint("");
|
||||||
} finally {
|
} finally {
|
||||||
setCheckingRole(false);
|
setCheckingRole(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setOtpDigit = (index: number, value: string) => {
|
const setOtpDigit = (index: number, value: string) => {
|
||||||
const clean = value.replace(/\D/g, '').slice(0, 1);
|
const clean = value.replace(/\D/g, "").slice(0, 1);
|
||||||
setOtp((prev) => {
|
setOtp((prev) => {
|
||||||
const next = prev.slice();
|
const next = prev.slice();
|
||||||
next[index] = clean;
|
next[index] = clean;
|
||||||
|
|
@ -151,60 +157,60 @@ export default function LoginRoute() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveUser = (user: any) => {
|
const saveUser = (user: any) => {
|
||||||
const fullName = String(user?.full_name || user?.fullName || '').trim();
|
const fullName = String(user?.full_name || user?.fullName || "").trim();
|
||||||
const [firstName, ...rest] = fullName.split(' ');
|
const [firstName, ...rest] = fullName.split(" ");
|
||||||
const lastName = rest.join(' ');
|
const lastName = rest.join(" ");
|
||||||
const normalizedRole = resolveActiveRole(
|
const normalizedRole = resolveActiveRole(
|
||||||
user?.active_role || user?.role || roleGuess(),
|
user?.active_role || user?.role || roleGuess(),
|
||||||
String(user?.email || email())
|
String(user?.email || email())
|
||||||
);
|
);
|
||||||
const storedRole = normalizedRole
|
const storedRole = normalizedRole ? normalizedRole.toLowerCase() : roleGuess();
|
||||||
? normalizedRole.toLowerCase()
|
|
||||||
: roleGuess();
|
|
||||||
const selectedProfessionalRole = getStoredPreferredRole(String(user?.email || email()));
|
const selectedProfessionalRole = getStoredPreferredRole(String(user?.email || email()));
|
||||||
const payload = {
|
const payload = {
|
||||||
firstName: firstName || '',
|
firstName: firstName || "",
|
||||||
lastName: lastName || '',
|
lastName: lastName || "",
|
||||||
fullName: fullName || '',
|
fullName: fullName || "",
|
||||||
name: fullName || '',
|
name: fullName || "",
|
||||||
displayName: fullName || '',
|
displayName: fullName || "",
|
||||||
email: String(user?.email || email()).trim().toLowerCase(),
|
email: String(user?.email || email())
|
||||||
|
.trim()
|
||||||
|
.toLowerCase(),
|
||||||
roleKey: storedRole,
|
roleKey: storedRole,
|
||||||
role: storedRole,
|
role: storedRole,
|
||||||
active_role: normalizedRole || 'JOB_SEEKER',
|
active_role: normalizedRole || "JOB_SEEKER",
|
||||||
selectedProfessionalRole: selectedProfessionalRole || null,
|
selectedProfessionalRole: selectedProfessionalRole || null,
|
||||||
user,
|
user,
|
||||||
};
|
};
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.localStorage.setItem('nxtgauge_auth_user', JSON.stringify(payload));
|
window.localStorage.setItem("nxtgauge_auth_user", JSON.stringify(payload));
|
||||||
window.localStorage.setItem('nxtgauge_user', JSON.stringify(payload));
|
window.localStorage.setItem("nxtgauge_user", JSON.stringify(payload));
|
||||||
window.localStorage.setItem('nxtgauge_signup_profile_v1', JSON.stringify(payload));
|
window.localStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify(payload));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
if (submitting()) return;
|
if (submitting()) return;
|
||||||
setError('');
|
setError("");
|
||||||
if (!isValidEmail(email())) {
|
if (!isValidEmail(email())) {
|
||||||
setError('Enter a valid email address.');
|
setError("Enter a valid email address.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!password().trim()) {
|
if (!password().trim()) {
|
||||||
setError('Password is required.');
|
setError("Password is required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!captchaInput().trim() || captchaInput().trim().toUpperCase() !== captcha().toUpperCase()) {
|
if (!captchaInput().trim() || captchaInput().trim().toUpperCase() !== captcha().toUpperCase()) {
|
||||||
setError('Captcha does not match. Please try again.');
|
setError("Captcha does not match. Please try again.");
|
||||||
setCaptcha(makeCaptcha());
|
setCaptcha(makeCaptcha());
|
||||||
setCaptchaInput('');
|
setCaptchaInput("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/gateway/api/auth/login', {
|
const res = await fetch("/api/gateway/api/auth/login", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email().trim().toLowerCase(),
|
email: email().trim().toLowerCase(),
|
||||||
password: password(),
|
password: password(),
|
||||||
|
|
@ -212,20 +218,20 @@ export default function LoginRoute() {
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const code = String(data?.code || '').toUpperCase();
|
const code = String(data?.code || "").toUpperCase();
|
||||||
if (code === 'EMAIL_NOT_VERIFIED') {
|
if (code === "EMAIL_NOT_VERIFIED") {
|
||||||
setShowVerify(true);
|
setShowVerify(true);
|
||||||
setError('Email not verified. Enter OTP sent to your inbox.');
|
setError("Email not verified. Enter OTP sent to your inbox.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(String(data?.error || data?.message || 'Invalid login credentials.'));
|
setError(String(data?.error || data?.message || "Invalid login credentials."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = String(data?.access_token || '').trim();
|
const accessToken = String(data?.access_token || "").trim();
|
||||||
if (typeof window !== 'undefined' && accessToken) {
|
if (typeof window !== "undefined" && accessToken) {
|
||||||
window.sessionStorage.setItem('nxtgauge_access_token', accessToken);
|
window.sessionStorage.setItem("nxtgauge_access_token", accessToken);
|
||||||
window.sessionStorage.setItem('nxtgauge_frontend_access_token', accessToken);
|
window.sessionStorage.setItem("nxtgauge_frontend_access_token", accessToken);
|
||||||
}
|
}
|
||||||
const resolvedActiveRole = resolveActiveRole(
|
const resolvedActiveRole = resolveActiveRole(
|
||||||
data?.user?.active_role || data?.user?.role,
|
data?.user?.active_role || data?.user?.role,
|
||||||
|
|
@ -233,9 +239,9 @@ export default function LoginRoute() {
|
||||||
);
|
);
|
||||||
const normalizedEmail = email().trim().toLowerCase();
|
const normalizedEmail = email().trim().toLowerCase();
|
||||||
const userPayload = {
|
const userPayload = {
|
||||||
id: String(data?.user?.id || ''),
|
id: String(data?.user?.id || ""),
|
||||||
email: String(data?.user?.email || normalizedEmail),
|
email: String(data?.user?.email || normalizedEmail),
|
||||||
full_name: String(data?.user?.full_name || ''),
|
full_name: String(data?.user?.full_name || ""),
|
||||||
active_role: resolvedActiveRole,
|
active_role: resolvedActiveRole,
|
||||||
email_verified: Boolean(data?.user?.email_verified ?? true),
|
email_verified: Boolean(data?.user?.email_verified ?? true),
|
||||||
};
|
};
|
||||||
|
|
@ -243,9 +249,9 @@ export default function LoginRoute() {
|
||||||
if (auth.setUser) {
|
if (auth.setUser) {
|
||||||
auth.setUser(userPayload);
|
auth.setUser(userPayload);
|
||||||
}
|
}
|
||||||
navigate('/dashboard', { replace: true });
|
navigate("/dashboard", { replace: true });
|
||||||
} catch {
|
} catch {
|
||||||
setError('Network error during login. Please try again.');
|
setError("Network error during login. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -253,18 +259,18 @@ export default function LoginRoute() {
|
||||||
|
|
||||||
const resendOtp = async () => {
|
const resendOtp = async () => {
|
||||||
if (submitting()) return;
|
if (submitting()) return;
|
||||||
setError('');
|
setError("");
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/gateway/api/auth/resend-otp', {
|
const res = await fetch("/api/auth/resend-otp", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
body: JSON.stringify({ email: email().trim().toLowerCase() }),
|
body: JSON.stringify({ email: email().trim().toLowerCase() }),
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setError(String(data?.error || data?.message || 'Unable to resend OTP.'));
|
setError(String(data?.error || data?.message || "Unable to resend OTP."));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
@ -273,22 +279,22 @@ export default function LoginRoute() {
|
||||||
|
|
||||||
const verifyThenLogin = async () => {
|
const verifyThenLogin = async () => {
|
||||||
if (submitting()) return;
|
if (submitting()) return;
|
||||||
setError('');
|
setError("");
|
||||||
if (otpCode().length !== 6) {
|
if (otpCode().length !== 6) {
|
||||||
setError('Enter a valid 6-digit OTP.');
|
setError("Enter a valid 6-digit OTP.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const verifyRes = await fetch('/api/gateway/api/auth/verify-email', {
|
const verifyRes = await fetch("/api/gateway/api/auth/verify-email", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
body: JSON.stringify({ otp: otpCode() }),
|
body: JSON.stringify({ otp: otpCode() }),
|
||||||
});
|
});
|
||||||
const verifyData = await verifyRes.json().catch(() => ({}));
|
const verifyData = await verifyRes.json().catch(() => ({}));
|
||||||
if (!verifyRes.ok) {
|
if (!verifyRes.ok) {
|
||||||
setError(String(verifyData?.error || verifyData?.message || 'OTP verification failed.'));
|
setError(String(verifyData?.error || verifyData?.message || "OTP verification failed."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await login();
|
await login();
|
||||||
|
|
@ -309,7 +315,9 @@ export default function LoginRoute() {
|
||||||
<div class="auth-visual-content">
|
<div class="auth-visual-content">
|
||||||
<p class="eyebrow">Public Workspace</p>
|
<p class="eyebrow">Public Workspace</p>
|
||||||
<h1 class="title light">Welcome Back To Nxtgauge</h1>
|
<h1 class="title light">Welcome Back To Nxtgauge</h1>
|
||||||
<p class="subtitle light">Sign in to manage your profile, portfolio, and verification in one place.</p>
|
<p class="subtitle light">
|
||||||
|
Sign in to manage your profile, portfolio, and verification in one place.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -317,7 +325,9 @@ export default function LoginRoute() {
|
||||||
<h2 class="title">Sign In</h2>
|
<h2 class="title">Sign In</h2>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="login-email">EMAIL</label>
|
<label class="label" for="login-email">
|
||||||
|
EMAIL
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="login-email"
|
id="login-email"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -333,25 +343,39 @@ export default function LoginRoute() {
|
||||||
}}
|
}}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
/>
|
/>
|
||||||
<p class="validation-note" style={{ color: email().trim() && isValidEmail(email()) ? '#fd6116' : '#6e7591' }}>
|
<p
|
||||||
{email().trim() && isValidEmail(email()) ? '✓ Valid email format' : '• Enter a valid email format'}
|
class="validation-note"
|
||||||
|
style={{ color: email().trim() && isValidEmail(email()) ? "#fd6116" : "#6e7591" }}
|
||||||
|
>
|
||||||
|
{email().trim() && isValidEmail(email())
|
||||||
|
? "✓ Valid email format"
|
||||||
|
: "• Enter a valid email format"}
|
||||||
</p>
|
</p>
|
||||||
<Show when={roleHint() || checkingRole()}>
|
<Show when={roleHint() || checkingRole()}>
|
||||||
<p class="validation-note" style={{ color: '#0f766e' }}>
|
<p class="validation-note" style={{ color: "#0f766e" }}>
|
||||||
{checkingRole() ? 'Checking account role...' : `• ${roleHint()}`}
|
{checkingRole() ? "Checking account role..." : `• ${roleHint()}`}
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="login-password">PASSWORD</label>
|
<label class="label" for="login-password">
|
||||||
|
PASSWORD
|
||||||
|
</label>
|
||||||
<div class="auth-password-wrap">
|
<div class="auth-password-wrap">
|
||||||
<input id="login-password" type={showPassword() ? 'text' : 'password'} class="input" value={password()} onInput={(e) => setPassword(e.currentTarget.value)} placeholder="Enter your password" />
|
<input
|
||||||
|
id="login-password"
|
||||||
|
type={showPassword() ? "text" : "password"}
|
||||||
|
class="input"
|
||||||
|
value={password()}
|
||||||
|
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="auth-toggle-visibility"
|
class="auth-toggle-visibility"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword((prev) => !prev)}
|
onClick={() => setShowPassword((prev) => !prev)}
|
||||||
aria-label={showPassword() ? 'Hide password' : 'Show password'}
|
aria-label={showPassword() ? "Hide password" : "Show password"}
|
||||||
>
|
>
|
||||||
<PasswordVisibilityIcon visible={showPassword()} />
|
<PasswordVisibilityIcon visible={showPassword()} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -361,9 +385,23 @@ export default function LoginRoute() {
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">CAPTCHA</label>
|
<label class="label">CAPTCHA</label>
|
||||||
<div class="auth-captcha-row">
|
<div class="auth-captcha-row">
|
||||||
<button class="auth-captcha-refresh" type="button" onClick={() => { setCaptcha(makeCaptcha()); setCaptchaInput(''); }}>↻</button>
|
<button
|
||||||
|
class="auth-captcha-refresh"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setCaptcha(makeCaptcha());
|
||||||
|
setCaptchaInput("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
<CaptchaCanvas code={captcha()} class="auth-captcha-canvas" />
|
<CaptchaCanvas code={captcha()} class="auth-captcha-canvas" />
|
||||||
<input class="input" value={captchaInput()} onInput={(e) => setCaptchaInput(e.currentTarget.value)} placeholder="Enter captcha" />
|
<input
|
||||||
|
class="input"
|
||||||
|
value={captchaInput()}
|
||||||
|
onInput={(e) => setCaptchaInput(e.currentTarget.value)}
|
||||||
|
placeholder="Enter captcha"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -385,26 +423,45 @@ export default function LoginRoute() {
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-footer-row">
|
<div class="auth-footer-row">
|
||||||
<p class="note">Didn’t receive code?</p>
|
<p class="note">Didn’t receive code?</p>
|
||||||
<button class="auth-forgot-link" type="button" onClick={() => void resendOtp()} disabled={submitting()}>
|
<button
|
||||||
|
class="auth-forgot-link"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void resendOtp()}
|
||||||
|
disabled={submitting()}
|
||||||
|
>
|
||||||
Resend OTP
|
Resend OTP
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<button class="auth-submit-btn" type="button" onClick={() => void login()} disabled={submitting()}>
|
<button
|
||||||
{submitting() ? 'Signing In...' : 'Sign In'}
|
class="auth-submit-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void login()}
|
||||||
|
disabled={submitting()}
|
||||||
|
>
|
||||||
|
{submitting() ? "Signing In..." : "Sign In"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Show when={showVerify()}>
|
<Show when={showVerify()}>
|
||||||
<button class="auth-submit-btn" type="button" onClick={() => void verifyThenLogin()} disabled={submitting()}>
|
<button
|
||||||
{submitting() ? 'Verifying...' : 'Verify Email and Login'}
|
class="auth-submit-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void verifyThenLogin()}
|
||||||
|
disabled={submitting()}
|
||||||
|
>
|
||||||
|
{submitting() ? "Verifying..." : "Verify Email and Login"}
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<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">
|
||||||
<p class="note"><A href="/forgot-password">Forgot Password?</A></p>
|
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()}>
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,7 @@ export default function SignupRoute() {
|
||||||
setServerError("");
|
setServerError("");
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/gateway/api/auth/resend-otp", {
|
const res = await fetch("/api/auth/resend-otp", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue