feat: login with username or email + change password
Backend: - Login handler accepts username OR email via the same field - New PUT /auth/me/password endpoint (verifies current password, requires min 8 chars for new password) Frontend: - Login form shows EMAIL OR USERNAME field (single field for both) - Auth store updated with changePassword function - UserSettings has new CHANGE PASSWORD section
This commit is contained in:
@@ -38,6 +38,7 @@ func (h *Handler) RegisterPublicRoutes(r chi.Router) {
|
||||
func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
|
||||
r.Get("/auth/me", h.Me)
|
||||
r.Patch("/auth/me", h.UpdateProfile)
|
||||
r.Put("/auth/me/password", h.ChangePassword)
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
@@ -49,6 +50,7 @@ type registerRequest struct {
|
||||
|
||||
type loginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -59,6 +61,11 @@ type updateProfileRequest struct {
|
||||
StatusText string `json:"status_text"`
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type UserProfile struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
@@ -150,10 +157,19 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
login := strings.TrimSpace(req.Email)
|
||||
if login == "" {
|
||||
login = strings.TrimSpace(req.Username)
|
||||
}
|
||||
if login == "" || req.Password == "" {
|
||||
http.Error(w, `{"error":"email/username and password required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var userID, hash string
|
||||
err := h.db.QueryRowContext(r.Context(), `
|
||||
SELECT id, password_hash FROM users WHERE email = $1
|
||||
`, req.Email).Scan(&userID, &hash)
|
||||
SELECT id, password_hash FROM users WHERE email = $1 OR username = $1
|
||||
`, login).Scan(&userID, &hash)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
|
||||
return
|
||||
@@ -295,3 +311,58 @@ func (h *Handler) getProfile(ctx context.Context, userID string) (UserProfile, e
|
||||
func (h *Handler) setSessionCookie(w http.ResponseWriter, token string) {
|
||||
SetSessionCookie(w, h.cfg.Session.CookieName, token, h.cfg.Session.Duration)
|
||||
}
|
||||
|
||||
// ChangePassword handles PUT /auth/me/password.
|
||||
func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req changePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.CurrentPassword == "" || req.NewPassword == "" {
|
||||
http.Error(w, `{"error":"current and new password required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.NewPassword) < 8 {
|
||||
http.Error(w, `{"error":"password must be at least 8 characters"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
var currentHash string
|
||||
err := h.db.QueryRowContext(r.Context(), "SELECT password_hash FROM users WHERE id = $1", userID).Scan(¤tHash)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ok, err = VerifyPassword(req.CurrentPassword, currentHash)
|
||||
if err != nil || !ok {
|
||||
http.Error(w, `{"error":"current password is incorrect"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
newHash, err := HashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.db.ExecContext(r.Context(), "UPDATE users SET password_hash = $1 WHERE id = $2", newHash, userID)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"failed to update password"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "password updated"})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../stores/auth.ts';
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuthStore } from "../stores/auth.ts";
|
||||
|
||||
export function LoginForm() {
|
||||
const [isRegister, setIsRegister] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [email, setEmail] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const { login, register, isLoading, error, clearError } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -16,44 +16,63 @@ export function LoginForm() {
|
||||
clearError();
|
||||
|
||||
if (isRegister) {
|
||||
if (password !== confirmPassword) {
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) return;
|
||||
await register(email, username, password);
|
||||
} else {
|
||||
await login(email, password);
|
||||
}
|
||||
|
||||
navigate('/');
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gb-bg p-4">
|
||||
<div className="terminal-border bg-gb-bg-h p-6 w-full max-w-md">
|
||||
<pre className="text-gb-orange font-mono text-lg mb-6">{'┌─ DUMPSTER ─┐'}</pre>
|
||||
<h2 className="text-gb-fg-s mb-4">{isRegister ? '[REGISTER]' : '[LOGIN]'}</h2>
|
||||
<pre className="text-gb-orange font-mono text-lg mb-6">
|
||||
{"┌─ DUMPSTER ─┐"}
|
||||
</pre>
|
||||
<h2 className="text-gb-fg-s mb-4">
|
||||
{isRegister ? "[REGISTER]" : "[LOGIN]"}
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">EMAIL:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
{isRegister && (
|
||||
{isRegister ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">EMAIL:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">USERNAME:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">USERNAME:</label>
|
||||
<label className="block text-gb-fg-t mb-1">
|
||||
EMAIL OR USERNAME:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete="username"
|
||||
placeholder="email or username"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -65,12 +84,16 @@ export function LoginForm() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete={isRegister ? 'new-password' : 'current-password'}
|
||||
autoComplete={
|
||||
isRegister ? "new-password" : "current-password"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{isRegister && (
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">CONFIRM PASSWORD:</label>
|
||||
<label className="block text-gb-fg-t mb-1">
|
||||
CONFIRM PASSWORD:
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
@@ -81,8 +104,16 @@ export function LoginForm() {
|
||||
</div>
|
||||
)}
|
||||
{error && <p className="text-gb-red text-sm">ERR: {error}</p>}
|
||||
<button type="submit" className="terminal-button w-full" disabled={isLoading}>
|
||||
{isLoading ? '[PROCESSING...]' : isRegister ? '[REGISTER]' : '[LOGIN]'}
|
||||
<button
|
||||
type="submit"
|
||||
className="terminal-button w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? "[PROCESSING...]"
|
||||
: isRegister
|
||||
? "[REGISTER]"
|
||||
: "[LOGIN]"}
|
||||
</button>
|
||||
</form>
|
||||
{!isRegister && (
|
||||
@@ -91,28 +122,29 @@ export function LoginForm() {
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
try {
|
||||
// Get challenge from server
|
||||
const resp = await fetch('/api/v1/auth/webauthn/login/begin', { method: 'POST', credentials: 'include' });
|
||||
if (!resp.ok) throw new Error('Passkey login not available');
|
||||
const resp = await fetch(
|
||||
"/api/v1/auth/webauthn/login/begin",
|
||||
{ method: "POST", credentials: "include" },
|
||||
);
|
||||
if (!resp.ok)
|
||||
throw new Error("Passkey login not available");
|
||||
const options = await resp.json();
|
||||
|
||||
// Use WebAuthn API
|
||||
const credential = await navigator.credentials.get({ publicKey: options });
|
||||
if (!credential) return;
|
||||
|
||||
// Send to server
|
||||
const finishResp = await fetch('/api/v1/auth/webauthn/login/finish', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credential),
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: options,
|
||||
});
|
||||
|
||||
if (finishResp.ok) {
|
||||
navigate('/');
|
||||
}
|
||||
if (!credential) return;
|
||||
const finishResp = await fetch(
|
||||
"/api/v1/auth/webauthn/login/finish",
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(credential),
|
||||
},
|
||||
);
|
||||
if (finishResp.ok) navigate("/");
|
||||
} catch (err) {
|
||||
console.error('Passkey login failed:', err);
|
||||
console.error("Passkey login failed:", err);
|
||||
}
|
||||
}}
|
||||
className="terminal-button w-full border-gb-aqua text-gb-aqua"
|
||||
@@ -130,12 +162,15 @@ export function LoginForm() {
|
||||
}}
|
||||
className="text-gb-blue hover:text-gb-aqua text-sm"
|
||||
>
|
||||
{isRegister ? '[BACK TO LOGIN]' : '[CREATE ACCOUNT]'}
|
||||
{isRegister ? "[BACK TO LOGIN]" : "[CREATE ACCOUNT]"}
|
||||
</button>
|
||||
</div>
|
||||
{!isRegister && (
|
||||
<p className="text-gb-fg-f text-xs mt-4 text-center">
|
||||
Use Link component: <Link to="/" className="text-gb-blue hover:text-gb-aqua">[HOME]</Link>
|
||||
Use Link component:{" "}
|
||||
<Link to="/" className="text-gb-blue hover:text-gb-aqua">
|
||||
[HOME]
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAuthStore } from '../stores/auth.ts';
|
||||
import { usePushStore } from '../stores/push.ts';
|
||||
|
||||
export function UserSettings() {
|
||||
const { user, updateProfile, isLoading, error, clearError, fetchMe } = useAuthStore();
|
||||
const { user, updateProfile, changePassword, isLoading, error, clearError, fetchMe } = useAuthStore();
|
||||
const { isSupported, isSubscribed, subscribe, unsubscribe, checkSubscription } = usePushStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -17,6 +17,14 @@ export function UserSettings() {
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Password change state
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||
const [passwordSuccess, setPasswordSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setDisplayName(user.display_name || '');
|
||||
@@ -86,6 +94,32 @@ export function UserSettings() {
|
||||
|
||||
const hexValid = /^#[0-9a-fA-F]{6}$/.test(accentColor);
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
setPasswordError(null);
|
||||
setPasswordSuccess(false);
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setPasswordError("new passwords do not match");
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
setPasswordError("password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
setPasswordLoading(true);
|
||||
try {
|
||||
await changePassword(currentPassword, newPassword);
|
||||
setPasswordSuccess(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmNewPassword('');
|
||||
setTimeout(() => setPasswordSuccess(false), 3000);
|
||||
} catch {
|
||||
// error is set in store
|
||||
} finally {
|
||||
setPasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gb-bg p-4 md:p-8 overflow-y-auto">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
@@ -234,6 +268,53 @@ export function UserSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Change Password */}
|
||||
<div className="border border-gb-bg-t p-4">
|
||||
<label className="block text-gb-fg-f mb-2 font-mono text-sm">
|
||||
CHANGE PASSWORD:
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
className="terminal-input w-full"
|
||||
placeholder="current password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="terminal-input w-full"
|
||||
placeholder="new password (min 8 chars)"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmNewPassword}
|
||||
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||
className="terminal-input w-full"
|
||||
placeholder="confirm new password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-gb-red text-xs font-mono">ERR: {passwordError}</p>
|
||||
)}
|
||||
{passwordSuccess && (
|
||||
<p className="text-gb-green text-xs font-mono">[password changed!]</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleChangePassword}
|
||||
disabled={passwordLoading || !currentPassword || newPassword.length < 8 || newPassword !== confirmNewPassword}
|
||||
className="terminal-button text-xs"
|
||||
>
|
||||
{passwordLoading ? '[CHANGING...]' : '[CHANGE PASSWORD]'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Push Notifications */}
|
||||
{isSupported && (
|
||||
<div className="border border-gb-bg-t p-4">
|
||||
|
||||
+57
-18
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { api } from '../lib/api.ts';
|
||||
import { create } from "zustand";
|
||||
import { api } from "../lib/api.ts";
|
||||
|
||||
export type UserStatus = 'online' | 'idle' | 'dnd' | 'offline';
|
||||
export type UserStatus = "online" | "idle" | "dnd" | "offline";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
@@ -28,11 +28,20 @@ interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (email: string, username: string, password: string, displayName?: string) => Promise<void>;
|
||||
login: (identifier: string, password: string) => Promise<void>;
|
||||
register: (
|
||||
email: string,
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
fetchMe: () => Promise<void>;
|
||||
updateProfile: (data: UpdateProfilePayload) => Promise<void>;
|
||||
changePassword: (
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
@@ -42,16 +51,19 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
login: async (email, password) => {
|
||||
login: async (identifier, password) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await api.post('/auth/login/password', { email, password });
|
||||
const user = await api.get<User>('/auth/me');
|
||||
await api.post("/auth/login/password", {
|
||||
email: identifier,
|
||||
password,
|
||||
});
|
||||
const user = await api.get<User>("/auth/me");
|
||||
set({ user, isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Login failed',
|
||||
error: error instanceof Error ? error.message : "Login failed",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -60,13 +72,19 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
register: async (email, username, password, displayName) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await api.post('/auth/register', { email, username, password, display_name: displayName || username });
|
||||
const user = await api.get<User>('/auth/me');
|
||||
await api.post("/auth/register", {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
display_name: displayName || username,
|
||||
});
|
||||
const user = await api.get<User>("/auth/me");
|
||||
set({ user, isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Registration failed',
|
||||
error:
|
||||
error instanceof Error ? error.message : "Registration failed",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -75,12 +93,12 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
logout: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await api.post('/auth/logout');
|
||||
await api.post("/auth/logout");
|
||||
set({ user: null, isAuthenticated: false, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Logout failed',
|
||||
error: error instanceof Error ? error.message : "Logout failed",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -89,14 +107,15 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
fetchMe: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const user = await api.get<User>('/auth/me');
|
||||
const user = await api.get<User>("/auth/me");
|
||||
set({ user, isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch user',
|
||||
error:
|
||||
error instanceof Error ? error.message : "Failed to fetch user",
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -104,12 +123,32 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
updateProfile: async (data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const user = await api.patch<User>('/auth/me', data);
|
||||
const user = await api.patch<User>("/auth/me", data);
|
||||
set({ user, isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Update failed',
|
||||
error: error instanceof Error ? error.message : "Update failed",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
changePassword: async (currentPassword, newPassword) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await api.put("/auth/me/password", {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Password change failed",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user