add password and display name manipulation
This commit is contained in:
parent
889914a57f
commit
1281c91c28
198
ACCOUNT_MANAGEMENT_IMPLEMENTATION.md
Normal file
198
ACCOUNT_MANAGEMENT_IMPLEMENTATION.md
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
# Account Management Implementation (Phase 4)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Phase 4 adds account management features allowing users to:
|
||||||
|
- Change their display name (friendly name separate from username)
|
||||||
|
- Change their password with current password verification
|
||||||
|
|
||||||
|
## Database Changes
|
||||||
|
|
||||||
|
### Migration: `add_display_name_column.sql`
|
||||||
|
```sql
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS display_name VARCHAR(100);
|
||||||
|
|
||||||
|
UPDATE users
|
||||||
|
SET display_name = name
|
||||||
|
WHERE display_name IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**To run migration:**
|
||||||
|
Connect to your PostgreSQL database and execute:
|
||||||
|
```bash
|
||||||
|
psql -U your_user -d your_database -f backend/migrations/add_display_name_column.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Implementation
|
||||||
|
|
||||||
|
### New Model Functions (`backend/models/user.model.js`)
|
||||||
|
- `getUserById(id)` - Fetch user by ID including display_name
|
||||||
|
- `updateUserProfile(id, updates)` - Update user profile (display_name)
|
||||||
|
- `updateUserPassword(id, hashedPassword)` - Update user password
|
||||||
|
- `getUserPasswordHash(id)` - Get current password hash for verification
|
||||||
|
|
||||||
|
### New Controllers (`backend/controllers/users.controller.js`)
|
||||||
|
- `getCurrentUser` - GET authenticated user's profile
|
||||||
|
- `updateCurrentUser` - PATCH user's display name
|
||||||
|
- Validates: 1-100 characters, trims whitespace
|
||||||
|
- `changePassword` - POST password change
|
||||||
|
- Validates: current password correct, new password min 6 chars, passwords match
|
||||||
|
- Uses bcrypt for password verification and hashing
|
||||||
|
|
||||||
|
### New Routes (`backend/routes/users.routes.js`)
|
||||||
|
All routes require authentication (`auth` middleware):
|
||||||
|
- `GET /api/users/me` - Get current user profile
|
||||||
|
- `PATCH /api/users/me` - Update display name
|
||||||
|
- `POST /api/users/me/change-password` - Change password
|
||||||
|
|
||||||
|
**Request bodies:**
|
||||||
|
```javascript
|
||||||
|
// Update display name
|
||||||
|
PATCH /api/users/me
|
||||||
|
{
|
||||||
|
"display_name": "New Display Name"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change password
|
||||||
|
POST /api/users/me/change-password
|
||||||
|
{
|
||||||
|
"current_password": "oldpass123",
|
||||||
|
"new_password": "newpass123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Implementation
|
||||||
|
|
||||||
|
### API Functions (`frontend/src/api/users.js`)
|
||||||
|
- `getCurrentUser()` - Fetch current user profile
|
||||||
|
- `updateCurrentUser(display_name)` - Update display name
|
||||||
|
- `changePassword(current_password, new_password)` - Change password
|
||||||
|
|
||||||
|
### Settings Page Updates (`frontend/src/pages/Settings.jsx`)
|
||||||
|
Added new "Account" tab with two sections:
|
||||||
|
|
||||||
|
**Display Name Section:**
|
||||||
|
- Input field with character counter (max 100)
|
||||||
|
- Real-time validation
|
||||||
|
- Save button with loading state
|
||||||
|
|
||||||
|
**Password Change Section:**
|
||||||
|
- Current password field
|
||||||
|
- New password field (min 6 chars)
|
||||||
|
- Confirm password field
|
||||||
|
- Client-side validation before submission
|
||||||
|
- Loading state during submission
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Success/error messages displayed at top of tab
|
||||||
|
- Form validation (character limits, password matching)
|
||||||
|
- Disabled buttons during API calls
|
||||||
|
- Auto-clears password fields on success
|
||||||
|
|
||||||
|
### Styling (`frontend/src/styles/pages/Settings.css`)
|
||||||
|
Added:
|
||||||
|
- `.account-form` - Form container styling
|
||||||
|
- `.account-message` - Success/error message styling
|
||||||
|
- `.account-message.success` - Green success messages
|
||||||
|
- `.account-message.error` - Red error messages
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Password Requirements
|
||||||
|
- **Backend validation:**
|
||||||
|
- Minimum 6 characters
|
||||||
|
- Current password verification before change
|
||||||
|
- bcrypt hashing (10 rounds)
|
||||||
|
|
||||||
|
- **Frontend validation:**
|
||||||
|
- HTML5 minlength attribute
|
||||||
|
- Client-side password matching check
|
||||||
|
- Current password required
|
||||||
|
|
||||||
|
### Display Name Validation
|
||||||
|
- **Backend:**
|
||||||
|
- 1-100 character limit
|
||||||
|
- Whitespace trimming
|
||||||
|
|
||||||
|
- **Frontend:**
|
||||||
|
- HTML5 maxlength attribute
|
||||||
|
- Character counter
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
1. Navigate to Settings → Account tab
|
||||||
|
2. **Change Display Name:**
|
||||||
|
- Enter new display name (1-100 chars)
|
||||||
|
- Click "Save Display Name"
|
||||||
|
3. **Change Password:**
|
||||||
|
- Enter current password
|
||||||
|
- Enter new password (min 6 chars)
|
||||||
|
- Confirm new password
|
||||||
|
- Click "Change Password"
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
**Testing the endpoints:**
|
||||||
|
```bash
|
||||||
|
# Get current user
|
||||||
|
curl -X GET http://localhost:5000/api/users/me \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
|
||||||
|
# Update display name
|
||||||
|
curl -X PATCH http://localhost:5000/api/users/me \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"display_name": "New Name"}'
|
||||||
|
|
||||||
|
# Change password
|
||||||
|
curl -X POST http://localhost:5000/api/users/me/change-password \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"current_password": "oldpass",
|
||||||
|
"new_password": "newpass"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Optional Enhancements
|
||||||
|
1. **Password strength indicator** - Visual feedback on password complexity
|
||||||
|
2. **Display name in navbar** - Show display_name instead of username in UI
|
||||||
|
3. **Email verification** - Add email field and verification
|
||||||
|
4. **2FA support** - Two-factor authentication option
|
||||||
|
5. **Password history** - Prevent reusing recent passwords
|
||||||
|
6. **Session management** - View/revoke active sessions
|
||||||
|
|
||||||
|
### Integration with AuthContext
|
||||||
|
Consider updating `AuthContext` to:
|
||||||
|
- Store and expose display_name
|
||||||
|
- Refresh user data after profile updates
|
||||||
|
- Show display_name in navbar/profile components
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- ✅ `backend/migrations/add_display_name_column.sql` (NEW)
|
||||||
|
- ✅ `backend/models/user.model.js`
|
||||||
|
- ✅ `backend/controllers/users.controller.js`
|
||||||
|
- ✅ `backend/routes/users.routes.js`
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- ✅ `frontend/src/api/users.js`
|
||||||
|
- ✅ `frontend/src/pages/Settings.jsx`
|
||||||
|
- ✅ `frontend/src/styles/pages/Settings.css`
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Run database migration
|
||||||
|
- [ ] Test GET /api/users/me endpoint
|
||||||
|
- [ ] Test display name update with valid data
|
||||||
|
- [ ] Test display name update with invalid data (empty, too long)
|
||||||
|
- [ ] Test password change with correct current password
|
||||||
|
- [ ] Test password change with incorrect current password
|
||||||
|
- [ ] Test password change with mismatched new passwords
|
||||||
|
- [ ] Test password change with weak password (< 6 chars)
|
||||||
|
- [ ] Verify frontend validation prevents invalid submissions
|
||||||
|
- [ ] Verify success/error messages display correctly
|
||||||
|
- [ ] Test UI responsiveness on mobile
|
||||||
@ -1,4 +1,5 @@
|
|||||||
const User = require("../models/user.model");
|
const User = require("../models/user.model");
|
||||||
|
const bcrypt = require("bcryptjs");
|
||||||
|
|
||||||
exports.test = async (req, res) => {
|
exports.test = async (req, res) => {
|
||||||
console.log("User route is working");
|
console.log("User route is working");
|
||||||
@ -45,11 +46,92 @@ exports.deleteUser = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
exports.checkIfUserExists = async (req, res) => {
|
exports.checkIfUserExists = async (req, res) => {
|
||||||
const { username } = req.query;
|
const { username } = req.query;
|
||||||
const users = await User.checkIfUserExists(username);
|
const exists = await User.checkIfUserExists(username);
|
||||||
res.json(users);
|
res.json(exists);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getCurrentUser = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const user = await User.getUserById(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error getting current user:", err);
|
||||||
|
res.status(500).json({ error: "Failed to get user profile" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.updateCurrentUser = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const { display_name } = req.body;
|
||||||
|
|
||||||
|
if (!display_name || display_name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: "Display name is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display_name.length > 100) {
|
||||||
|
return res.status(400).json({ error: "Display name must be 100 characters or less" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await User.updateUserProfile(userId, { display_name: display_name.trim() });
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return res.status(404).json({ error: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: "Profile updated successfully", user: updated });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error updating user profile:", err);
|
||||||
|
res.status(500).json({ error: "Failed to update profile" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.changePassword = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const { current_password, new_password } = req.body;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!current_password || !new_password) {
|
||||||
|
return res.status(400).json({ error: "Current password and new password are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_password.length < 6) {
|
||||||
|
return res.status(400).json({ error: "New password must be at least 6 characters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current password hash
|
||||||
|
const currentHash = await User.getUserPasswordHash(userId);
|
||||||
|
|
||||||
|
if (!currentHash) {
|
||||||
|
return res.status(404).json({ error: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify current password
|
||||||
|
const isValidPassword = await bcrypt.compare(current_password, currentHash);
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
return res.status(401).json({ error: "Current password is incorrect" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash new password
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const hashedPassword = await bcrypt.hash(new_password, salt);
|
||||||
|
|
||||||
|
// Update password
|
||||||
|
await User.updateUserPassword(userId, hashedPassword);
|
||||||
|
|
||||||
|
res.json({ message: "Password changed successfully" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error changing password:", err);
|
||||||
|
res.status(500).json({ error: "Failed to change password" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
10
backend/migrations/add_display_name_column.sql
Normal file
10
backend/migrations/add_display_name_column.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- Add display_name column to users table
|
||||||
|
-- This allows users to have a friendly name separate from their username
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS display_name VARCHAR(100);
|
||||||
|
|
||||||
|
-- Set display_name to name for existing users (as default)
|
||||||
|
UPDATE users
|
||||||
|
SET display_name = name
|
||||||
|
WHERE display_name IS NULL;
|
||||||
@ -24,10 +24,49 @@ exports.createUser = async (username, hashedPassword, name) => {
|
|||||||
|
|
||||||
|
|
||||||
exports.getAllUsers = async () => {
|
exports.getAllUsers = async () => {
|
||||||
const result = await pool.query("SELECT id, username, name, role FROM users ORDER BY id ASC");
|
const result = await pool.query("SELECT id, username, name, role, display_name FROM users ORDER BY id ASC");
|
||||||
return result.rows;
|
return result.rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getUserById = async (id) => {
|
||||||
|
const result = await pool.query(
|
||||||
|
"SELECT id, username, name, role, display_name FROM users WHERE id = $1",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.updateUserProfile = async (id, updates) => {
|
||||||
|
const { display_name } = updates;
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE users
|
||||||
|
SET display_name = COALESCE($1, display_name)
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING id, username, name, role, display_name`,
|
||||||
|
[display_name, id]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.updateUserPassword = async (id, hashedPassword) => {
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE users
|
||||||
|
SET password = $1
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING id`,
|
||||||
|
[hashedPassword, id]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getUserPasswordHash = async (id) => {
|
||||||
|
const result = await pool.query(
|
||||||
|
"SELECT password FROM users WHERE id = $1",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
return result.rows[0]?.password;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.updateUserRole = async (id, role) => {
|
exports.updateUserRole = async (id, role) => {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
|
|||||||
@ -7,4 +7,9 @@ const { ROLES } = require("../models/user.model");
|
|||||||
router.get("/exists", usersController.checkIfUserExists);
|
router.get("/exists", usersController.checkIfUserExists);
|
||||||
router.get("/test", usersController.test);
|
router.get("/test", usersController.test);
|
||||||
|
|
||||||
|
// Current user profile routes (authenticated)
|
||||||
|
router.get("/me", auth, usersController.getCurrentUser);
|
||||||
|
router.patch("/me", auth, usersController.updateCurrentUser);
|
||||||
|
router.post("/me/change-password", auth, usersController.changePassword);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -3,4 +3,16 @@ import api from "./axios";
|
|||||||
export const getAllUsers = () => api.get("/admin/users");
|
export const getAllUsers = () => api.get("/admin/users");
|
||||||
export const updateRole = (id, role) => api.put(`/admin/users`, { id, role });
|
export const updateRole = (id, role) => api.put(`/admin/users`, { id, role });
|
||||||
export const deleteUser = (id) => api.delete(`/admin/users/${id}`);
|
export const deleteUser = (id) => api.delete(`/admin/users/${id}`);
|
||||||
export const checkIfUserExists = (username) => api.get(`/users/exists`, { params: { username: username } });
|
export const checkIfUserExists = (username) => api.get(`/users/exists`, { params: { username: username } });
|
||||||
|
|
||||||
|
export const getCurrentUser = async () => {
|
||||||
|
return api.get("/users/me");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCurrentUser = async (display_name) => {
|
||||||
|
return api.patch("/users/me", { display_name });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changePassword = async (current_password, new_password) => {
|
||||||
|
return api.post("/users/me/change-password", { current_password, new_password });
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { useContext, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { changePassword, getCurrentUser, updateCurrentUser } from "../api/users";
|
||||||
import { SettingsContext } from "../context/SettingsContext";
|
import { SettingsContext } from "../context/SettingsContext";
|
||||||
import "../styles/pages/Settings.css";
|
import "../styles/pages/Settings.css";
|
||||||
|
|
||||||
@ -7,11 +8,84 @@ export default function Settings() {
|
|||||||
const { settings, updateSettings, resetSettings } = useContext(SettingsContext);
|
const { settings, updateSettings, resetSettings } = useContext(SettingsContext);
|
||||||
const [activeTab, setActiveTab] = useState("appearance");
|
const [activeTab, setActiveTab] = useState("appearance");
|
||||||
|
|
||||||
|
// Account management state
|
||||||
|
const [displayName, setDisplayName] = useState("");
|
||||||
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [accountMessage, setAccountMessage] = useState({ type: "", text: "" });
|
||||||
|
const [loadingProfile, setLoadingProfile] = useState(false);
|
||||||
|
const [loadingPassword, setLoadingPassword] = useState(false);
|
||||||
|
|
||||||
|
// Load user profile
|
||||||
|
useEffect(() => {
|
||||||
|
const loadProfile = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getCurrentUser();
|
||||||
|
setDisplayName(response.data.display_name || response.data.name || "");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load profile:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadProfile();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleThemeChange = (theme) => {
|
const handleThemeChange = (theme) => {
|
||||||
updateSettings({ theme });
|
updateSettings({ theme });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateDisplayName = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoadingProfile(true);
|
||||||
|
setAccountMessage({ type: "", text: "" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateCurrentUser(displayName);
|
||||||
|
setAccountMessage({ type: "success", text: "Display name updated successfully!" });
|
||||||
|
} catch (error) {
|
||||||
|
setAccountMessage({
|
||||||
|
type: "error",
|
||||||
|
text: error.response?.data?.error || "Failed to update display name"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoadingProfile(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePassword = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoadingPassword(true);
|
||||||
|
setAccountMessage({ type: "", text: "" });
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setAccountMessage({ type: "error", text: "New passwords don't match" });
|
||||||
|
setLoadingPassword(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
setAccountMessage({ type: "error", text: "Password must be at least 6 characters" });
|
||||||
|
setLoadingPassword(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await changePassword(currentPassword, newPassword);
|
||||||
|
setAccountMessage({ type: "success", text: "Password changed successfully!" });
|
||||||
|
setCurrentPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
} catch (error) {
|
||||||
|
setAccountMessage({
|
||||||
|
type: "error",
|
||||||
|
text: error.response?.data?.error || "Failed to change password"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoadingPassword(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleToggle = (key) => {
|
const handleToggle = (key) => {
|
||||||
updateSettings({ [key]: !settings[key] });
|
updateSettings({ [key]: !settings[key] });
|
||||||
@ -59,6 +133,12 @@ export default function Settings() {
|
|||||||
>
|
>
|
||||||
Behavior
|
Behavior
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`settings-tab ${activeTab === "account" ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("account")}
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-content">
|
<div className="settings-content">
|
||||||
@ -237,6 +317,102 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Account Tab */}
|
||||||
|
{activeTab === "account" && (
|
||||||
|
<div className="settings-section">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Account Management</h2>
|
||||||
|
|
||||||
|
{accountMessage.text && (
|
||||||
|
<div className={`account-message ${accountMessage.type}`}>
|
||||||
|
{accountMessage.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Display Name Section */}
|
||||||
|
<form onSubmit={handleUpdateDisplayName} className="account-form">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Display Name</h3>
|
||||||
|
<div className="settings-group">
|
||||||
|
<label className="settings-label">
|
||||||
|
Display Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
maxLength={100}
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Your display name"
|
||||||
|
/>
|
||||||
|
<p className="settings-description">
|
||||||
|
{displayName.length}/100 characters
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={loadingProfile}
|
||||||
|
>
|
||||||
|
{loadingProfile ? "Saving..." : "Save Display Name"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr className="my-4" style={{ borderColor: 'var(--border-color)' }} />
|
||||||
|
|
||||||
|
{/* Password Change Section */}
|
||||||
|
<form onSubmit={handleChangePassword} className="account-form">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Change Password</h3>
|
||||||
|
<div className="settings-group">
|
||||||
|
<label className="settings-label">
|
||||||
|
Current Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="settings-group">
|
||||||
|
<label className="settings-label">
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
minLength={6}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="settings-description">
|
||||||
|
Minimum 6 characters
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="settings-group">
|
||||||
|
<label className="settings-label">
|
||||||
|
Confirm New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
minLength={6}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={loadingPassword}
|
||||||
|
>
|
||||||
|
{loadingPassword ? "Changing..." : "Change Password"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
|
|||||||
@ -188,3 +188,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Account Management */
|
||||||
|
.account-form {
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-message {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-message.success {
|
||||||
|
background-color: var(--color-success-bg);
|
||||||
|
color: var(--color-success);
|
||||||
|
border: 1px solid var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-message.error {
|
||||||
|
background-color: var(--color-danger-bg);
|
||||||
|
color: var(--color-danger);
|
||||||
|
border: 1px solid var(--color-danger);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user