Compare commits

...

2 Commits

8 changed files with 72 additions and 24 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Qlockify.ir | Time Tracking for Modern Teams</title> <title>Qlockify</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -334,7 +334,7 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) =>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto"> <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
<div className="flex shrink-0 items-center justify-between border-b border-slate-200 px-5 py-5 dark:border-slate-800"> <div className="flex shrink-0 items-center justify-between border-b border-slate-200 px-5 py-5 dark:border-slate-800">
<h2 className="truncate text-lg font-bold text-slate-800 dark:text-white"> <h2 className="truncate text-lg font-bold text-slate-800 dark:text-white">
Qlockify.ir {t.title || "Qlockify"}
</h2> </h2>
<button <button

View File

@@ -60,6 +60,9 @@ export const en = {
newPasswordPlaceholder: "New password", newPasswordPlaceholder: "New password",
confirmPasswordPlaceholder: "Confirm password", confirmPasswordPlaceholder: "Confirm password",
passwordMismatch: "The password confirmation does not match.", passwordMismatch: "The password confirmation does not match.",
passwordRequirements:
"Password must be at least 8 characters and include at least one lowercase letter, one uppercase letter, one digit, and one symbol.",
passwordReuse: "New password must be different from your previous password.",
enterPassword: "Enter your password", enterPassword: "Enter your password",
verifyNumber: "Verify your number", verifyNumber: "Verify your number",
enterMobileDesc: "Enter your mobile number to continue", enterMobileDesc: "Enter your mobile number to continue",

View File

@@ -59,6 +59,9 @@ export const fa = {
newPasswordPlaceholder: "رمز عبور جدید", newPasswordPlaceholder: "رمز عبور جدید",
confirmPasswordPlaceholder: "تکرار رمز عبور", confirmPasswordPlaceholder: "تکرار رمز عبور",
passwordMismatch: "رمز عبور و تکرار آن یکسان نیستند.", passwordMismatch: "رمز عبور و تکرار آن یکسان نیستند.",
passwordRequirements:
"رمز عبور باید حداقل 8 کاراکتر باشد و حداقل یک حرف کوچک، یک حرف بزرگ، یک عدد و یک نماد داشته باشد.",
passwordReuse: "رمز عبور جدید نباید با رمز عبور قبلی یکسان باشد.",
welcome: (title: string = "Qlockifiy") => `به ${title} خوش آمدید`, welcome: (title: string = "Qlockifiy") => `به ${title} خوش آمدید`,
enterPassword: "رمز عبور خود را وارد کنید", enterPassword: "رمز عبور خود را وارد کنید",
verifyNumber: "تایید شماره موبایل", verifyNumber: "تایید شماره موبایل",

View File

@@ -16,6 +16,7 @@ import { Modal } from "../components/Modal"
import { Input } from "../components/ui/input" import { Input } from "../components/ui/input"
import { TextAreaInput } from "../components/ui/TextAreaInput" import { TextAreaInput } from "../components/ui/TextAreaInput"
import { AuthPasswordField } from "./auth/AuthPasswordField" import { AuthPasswordField } from "./auth/AuthPasswordField"
import { getPasswordValidationMessage } from "./auth/utils"
export interface UserProfile { export interface UserProfile {
id?: string; id?: string;
@@ -192,6 +193,17 @@ export default function Profile() {
return return
} }
const passwordValidationMessage = getPasswordValidationMessage(passwordForm.newPassword, t.login)
if (passwordValidationMessage) {
toast.error(passwordValidationMessage)
return
}
if (passwordForm.currentPassword === passwordForm.newPassword) {
toast.error(t.login.passwordReuse)
return
}
setIsSaving(true) setIsSaving(true)
try { try {
await changePassword( await changePassword(

View File

@@ -9,7 +9,7 @@ import { useAuthFlow } from "../../context/AuthFlowContext"
import { useTranslation } from "../../hooks/useTranslation" import { useTranslation } from "../../hooks/useTranslation"
import { AuthPanel } from "./AuthPanel" import { AuthPanel } from "./AuthPanel"
import { AuthPasswordField } from "./AuthPasswordField" import { AuthPasswordField } from "./AuthPasswordField"
import { getApiErrorMessage } from "./utils" import { getApiErrorMessage, getPasswordValidationMessage } from "./utils"
export function ForgotPasswordPasswordPage() { export function ForgotPasswordPasswordPage() {
const navigate = useNavigate() const navigate = useNavigate()
@@ -40,6 +40,12 @@ export function ForgotPasswordPasswordPage() {
return return
} }
const passwordValidationMessage = getPasswordValidationMessage(password, t.login)
if (passwordValidationMessage) {
toast.error(passwordValidationMessage)
return
}
setLoading(true) setLoading(true)
try { try {
await resetPasswordWithOtp(state.forgotPassword.mobile, state.forgotPassword.code, password, confirmation) await resetPasswordWithOtp(state.forgotPassword.mobile, state.forgotPassword.code, password, confirmation)

View File

@@ -9,7 +9,7 @@ import { useAuthFlow } from "../../context/AuthFlowContext"
import { useTranslation } from "../../hooks/useTranslation" import { useTranslation } from "../../hooks/useTranslation"
import { AuthPanel } from "./AuthPanel" import { AuthPanel } from "./AuthPanel"
import { AuthPasswordField } from "./AuthPasswordField" import { AuthPasswordField } from "./AuthPasswordField"
import { completeAuthentication, getApiErrorMessage } from "./utils" import { completeAuthentication, getApiErrorMessage, getPasswordValidationMessage } from "./utils"
export function SignupPasswordPage() { export function SignupPasswordPage() {
const navigate = useNavigate() const navigate = useNavigate()
@@ -40,6 +40,12 @@ export function SignupPasswordPage() {
return return
} }
const passwordValidationMessage = getPasswordValidationMessage(password, t.login)
if (passwordValidationMessage) {
toast.error(passwordValidationMessage)
return
}
setLoading(true) setLoading(true)
try { try {
const data = await registerWithOtp(state.signup.mobile, state.signup.code, password, confirmation) const data = await registerWithOtp(state.signup.mobile, state.signup.code, password, confirmation)

View File

@@ -29,6 +29,24 @@ export const getApiErrorMessage = (error: unknown, fallbackMessage: string) => {
return fallbackMessage return fallbackMessage
} }
export const getPasswordValidationMessage = (
password: string,
copy: {
passwordRequirements: string
},
) => {
const hasLowercase = /[a-z]/.test(password)
const hasUppercase = /[A-Z]/.test(password)
const hasDigit = /\d/.test(password)
const hasSymbol = /[^A-Za-z0-9]/.test(password)
if (password.length < 8 || !hasLowercase || !hasUppercase || !hasDigit || !hasSymbol) {
return copy.passwordRequirements
}
return null
}
export const handleThrottleError = ({ export const handleThrottleError = ({
error, error,
cooldownKey, cooldownKey,