Skip to main content

Changing Password

This guide covers how to change your password, password requirements, and account security best practices.


Overview

When to Change Your Password

Change your password in these situations:

SituationPriorityAction
First LoginRequiredMust change temporary password
Security ConcernHighChange immediately
Regular RotationMediumEvery 90 days recommended
Shared DeviceHighChange after use

Password Change Flow

Enter Current Password → Verify → Enter New Password → Confirm → Update

Changing Your Password

Access Password Change

  1. Navigate to My Profile in Staff Portal
  2. Click Change Password or Security
  3. The password change form appears

Change Password API

Endpoint: POST /api/auth/change-password

// Source: src/app/api/auth/change-password/route.ts:15-127
const changePasswordSchema = {
currentPassword: 'string (required)',
newPassword: 'string (required)'
}

Request Format

// Change password request
const changePassword = async (currentPassword: string, newPassword: string) => {
const response = await fetch('/api/auth/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentPassword,
newPassword
})
})

return response.json()
}

Success Response

// Successful password change
{
success: true,
message: "Password updated successfully"
}

Password Requirements

Minimum Requirements

RequirementMinimumRecommended
Length8 characters12+ characters
Uppercase1 letter2+ letters
Lowercase1 letter2+ letters
Numbers1 digit2+ digits
Special Characters1 symbol2+ symbols

Valid Special Characters

! @ # $ % ^ & * ( ) - _ = + [ ] { } ; : ' " , . < > / ? \ |

Password Strength Indicators

StrengthCriteriaColour
Weak< 8 chars or missing requirements🔴 Red
Fair8-10 chars, meets basic requirements🟡 Yellow
Good11-14 chars, exceeds requirements🟢 Green
Strong15+ chars, complex mix🔵 Blue

Password Validation

// Password validation function
const validatePassword = (password: string): ValidationResult => {
const errors: string[] = []

if (password.length < 8) {
errors.push('Password must be at least 8 characters')
}

if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter')
}

if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter')
}

if (!/\d/.test(password)) {
errors.push('Password must contain at least one number')
}

if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
errors.push('Password must contain at least one special character')
}

return {
valid: errors.length === 0,
errors,
strength: calculateStrength(password)
}
}

Password Change Process

Step 1: Enter Current Password

// Source: src/app/api/auth/change-password/route.ts:52-59
// Request body parsing
const { currentPassword, newPassword } = await request.json()

if (!currentPassword || !newPassword) {
return NextResponse.json(
{ error: 'Current password and new password are required' },
{ status: 400 }
)
}

Step 2: Verify Current Password

// Source: src/app/api/auth/change-password/route.ts:64-82
// Verify current password by attempting to sign in
const { error: verifyError } = await verifierClient.auth.signInWithPassword({
email: user.email || '',
password: currentPassword
})

if (verifyError) {
return NextResponse.json(
{ error: 'Current password is incorrect' },
{ status: 400 }
)
}

Step 3: Update Password

// Source: src/app/api/auth/change-password/route.ts:89-102
// Update password and metadata
const { error: updateError } = await supabase.auth.updateUser({
password: newPassword,
data: {
password_change_required: false
}
})

if (updateError) {
return NextResponse.json(
{ error: updateError.message || 'Failed to update password' },
{ status: 500 }
)
}

Step 4: Update Staff Record

// Source: src/app/api/auth/change-password/route.ts:104-110
// Update staff table to clear password_change_required flag
await supabase
.from('staff')
.update({
password_change_required: false,
updated_at: new Date().toISOString()
})
.eq('id', user.id)

First-Time Login

Required Password Change

When you first login with a temporary password:

  1. Login with provided credentials
  2. Modal appears requiring password change
  3. Cannot proceed until password is changed
  4. Set new password meeting all requirements

First Login Detection

// Check if password change is required
const checkPasswordChangeRequired = async () => {
const { data: { user } } = await supabase.auth.getUser()

if (user?.user_metadata?.password_change_required) {
showPasswordChangeModal()
}
}

First Login Modal

// First-time login password change modal
const FirstLoginPasswordModal = ({ onComplete }) => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="card-glass p-6 max-w-md w-full mx-4">
<h2 className="text-xl font-semibold mb-4">Change Your Password</h2>
<p className="text-secondary-text mb-6">
For security reasons, you must change your temporary password before continuing.
</p>

<PasswordChangeForm
requireCurrentPassword={false}
onSuccess={onComplete}
/>
</div>
</div>
)

Password Change Form

Form Component

// Password change form component
const PasswordChangeForm = ({
requireCurrentPassword = true,
onSuccess,
onError
}) => {
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')

// Validate passwords match
if (newPassword !== confirmPassword) {
setError('New passwords do not match')
return
}

// Validate password strength
const validation = validatePassword(newPassword)
if (!validation.valid) {
setError(validation.errors.join(', '))
return
}

setLoading(true)

try {
const response = await fetch('/api/auth/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentPassword: requireCurrentPassword ? currentPassword : undefined,
newPassword
})
})

const result = await response.json()

if (!response.ok) {
throw new Error(result.error || 'Failed to change password')
}

onSuccess?.()
} catch (err) {
setError(err.message)
onError?.(err)
} finally {
setLoading(false)
}
}

return (
<form onSubmit={handleSubmit} className="space-y-4">
{requireCurrentPassword && (
<div>
<label className="block text-sm font-medium mb-1">
Current Password
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="form-input w-full"
required
/>
</div>
)}

<div>
<label className="block text-sm font-medium mb-1">
New Password
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="form-input w-full"
required
minLength={8}
/>
<PasswordStrengthIndicator password={newPassword} />
</div>

<div>
<label className="block text-sm font-medium mb-1">
Confirm New Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="form-input w-full"
required
/>
</div>

{error && (
<div className="p-3 bg-red-500/20 rounded text-red-200 text-sm">
{error}
</div>
)}

<button
type="submit"
disabled={loading}
className="form-button w-full"
>
{loading ? 'Changing Password...' : 'Change Password'}
</button>
</form>
)
}

Strength Indicator

// Password strength indicator component
const PasswordStrengthIndicator = ({ password }: { password: string }) => {
const strength = calculateStrength(password)

const colours = {
weak: 'bg-red-500',
fair: 'bg-yellow-500',
good: 'bg-green-500',
strong: 'bg-blue-500'
}

const widths = {
weak: 'w-1/4',
fair: 'w-2/4',
good: 'w-3/4',
strong: 'w-full'
}

if (!password) return null

return (
<div className="mt-2">
<div className="h-1 bg-white/10 rounded overflow-hidden">
<div
className={`h-full ${colours[strength]} ${widths[strength]} transition-all`}
/>
</div>
<p className="text-xs text-secondary-text mt-1">
Password strength: {strength.charAt(0).toUpperCase() + strength.slice(1)}
</p>
</div>
)
}

Password Reset

Forgot Password

If you've forgotten your password:

  1. Go to Login page
  2. Click "Forgot Password"
  3. Enter your email address
  4. Request is sent to your Company Manager
  5. Manager approves the reset request
  6. Receive new temporary password

Password Reset Request API

Endpoint: POST /api/auth/password-reset/request

// Request password reset
const requestPasswordReset = async (email: string) => {
const response = await fetch('/api/auth/password-reset/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
})

return response.json()
}

Reset Approval Workflow

StepActorAction
1StaffRequests password reset
2SystemCreates reset request
3ManagerReviews request
4ManagerApproves or rejects
5SystemGenerates temporary password
6StaffReceives notification
7StaffLogs in and changes password

Security Best Practices

Do's and Don'ts

✅ Do❌ Don't
Use unique passwordsReuse passwords from other sites
Use a password managerWrite passwords down
Change regularlyShare your password
Log out of shared devicesUse personal info in passwords
Report suspicious activityIgnore security warnings

Creating Strong Passwords

Good Password Pattern:

[Word][Number][Symbol][Word][Number]

Example: Dolphin47!Azure22

Passphrase Method:

[Word]-[Word]-[Word]-[Number]

Example: correct-horse-battery-42

Password Storage

Never Store Passwords
  • Don't write passwords on paper
  • Don't save in browser (shared computers)
  • Don't share via email or messages
  • Use a password manager instead

Error Handling

Common Errors

ErrorCauseSolution
Current password incorrectWrong current passwordRe-enter correctly
Password too weakDoesn't meet requirementsAdd complexity
Passwords don't matchConfirmation mismatchRe-type carefully
Authentication requiredSession expiredLogin again

Error Messages

// Error response examples
{
error: "Current password is incorrect"
// Action: Re-enter your current password
}

{
error: "Password must be at least 8 characters"
// Action: Use a longer password
}

{
error: "Failed to update password"
// Action: Try again or contact support
}

Handling Errors in UI

// Error handling in form
const handlePasswordError = (error: string) => {
switch (error) {
case 'Current password is incorrect':
// Highlight current password field
setFieldError('currentPassword', error)
break

case 'Password must be at least 8 characters':
// Show requirement hint
setFieldError('newPassword', error)
break

default:
// Show general error
setGeneralError(error)
}
}

Account Security

Session Management

After changing your password:

  • Current session remains active
  • Other sessions are not invalidated
  • Consider logging out of other devices manually

Account Lockout

ConditionResult
3 failed attemptsWarning displayed
5 failed attemptsAccount locked 15 minutes
10 failed attemptsAccount locked 1 hour
Further attemptsContact manager required

Lockout Recovery

// Check if account is locked
const checkAccountStatus = async (email: string) => {
const { data: staff } = await supabase
.from('staff')
.select('locked_until, failed_login_attempts')
.eq('email', email)
.single()

if (staff?.locked_until && new Date(staff.locked_until) > new Date()) {
return {
locked: true,
until: staff.locked_until,
message: `Account locked until ${formatTime(staff.locked_until)}`
}
}

return { locked: false }
}

Two-Factor Authentication

Future Feature

Coming Soon

Two-factor authentication (2FA) will be available in a future update. This will add an extra layer of security requiring:

  • Password
  • One-time code from authenticator app

Preparing for 2FA

When 2FA becomes available:

  1. Download authenticator app (Google Authenticator, Authy)
  2. Enable 2FA in profile settings
  3. Scan QR code
  4. Save backup codes securely

Mobile Password Change

Mobile Form

// Mobile password change form
const MobilePasswordChange = () => (
<div className="p-4 space-y-4">
<h2 className="text-lg font-semibold">Change Password</h2>

<input
type="password"
placeholder="Current Password"
className="form-input w-full text-base py-3"
autoComplete="current-password"
/>

<input
type="password"
placeholder="New Password"
className="form-input w-full text-base py-3"
autoComplete="new-password"
/>

<input
type="password"
placeholder="Confirm New Password"
className="form-input w-full text-base py-3"
autoComplete="new-password"
/>

<button className="form-button w-full py-3 text-base">
Change Password
</button>
</div>
)

Touch Considerations

ElementMinimum Size
Input fields48px height
Submit button48px height
Show/hide toggle44px × 44px

Troubleshooting

Common Issues

IssueCauseSolution
Can't change passwordSession expiredLogin again
Strength check failingMissing requirementAdd required elements
Current password wrongCaps lock or typoCheck input carefully
Form not submittingValidation errorCheck all fields

Getting Help

If you cannot change your password:

  1. Try again - Check for typos
  2. Request reset - Use forgot password
  3. Contact manager - Ask for assistance
  4. Technical support - System Admin help

Audit Trail

Password Change Logging

// Source: src/app/api/auth/change-password/route.ts:87
// Audit logging for password changes
logMetadataChange(user.id, 'updateUser', { password_change_required: false })

What Gets Logged

EventLoggedDetails
Password changed✅ YesUser ID, timestamp
Change attempted✅ YesUser ID, success/failure
Current password❌ NoNever logged
New password❌ NoNever logged


Source Files:

  • src/app/api/auth/change-password/route.ts - Password change API
  • src/app/api/auth/password-reset/request/route.ts - Password reset request
  • src/app/api/auth/password-reset/approve/route.ts - Reset approval
  • src/lib/utils/audit.ts - Audit logging utilities