Add password management to admin users page

- Add PATCH endpoint to set user passwords
- Add password modal UI in admin panel
- Update CLAUDE.md with latest features

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-16 15:12:23 +05:30
parent 0349be2067
commit c65051cf7d
4 changed files with 120 additions and 17 deletions

View file

@ -42,7 +42,7 @@ src/
│ ├── menu/ # Navigation menu │ ├── menu/ # Navigation menu
│ ├── onboarding/ # First-time setup │ ├── onboarding/ # First-time setup
│ ├── settings/ # Settings with theme picker │ ├── settings/ # Settings with theme picker
│ ├── login/ # User login (magic) │ ├── login/ # User login (email + password)
│ ├── admin/ # Admin panel │ ├── admin/ # Admin panel
│ └── admin-login/ # Admin login (separate) │ └── admin-login/ # Admin login (separate)
├── ThemeProvider.tsx # Theme context (light/dark/system/time) ├── ThemeProvider.tsx # Theme context (light/dark/system/time)
@ -67,6 +67,7 @@ docs/ # Design docs
- **Vaccinations:** IAP schedule tracking - **Vaccinations:** IAP schedule tracking
- **Growth:** Weight/height over time - **Growth:** Weight/height over time
- **Memories:** Photos with R2 storage - **Memories:** Photos with R2 storage
- **Chat Sessions:** User conversations with AI (chat_sessions, chat_messages)
### Key Patterns ### Key Patterns
@ -94,7 +95,7 @@ const { familyId, familyName, child, children, tier, memberCount } = useFamily()
**Offline Queue:** Uses localStorage (`tia_offline_queue`) for failed API calls, retries when online. **Offline Queue:** Uses localStorage (`tia_offline_queue`) for failed API calls, retries when online.
**Chat Sessions:** Stored in localStorage (`tia_chat_sessions`) - shared between home page AI card and /ai page. **Chat Sessions:** Stored in localStorage (`tia_chat_sessions`) - shared between home page AI card and /ai page. Database tables: `chat_sessions`, `chat_messages`.
**API Routes:** Return standard JSON `{ success: true, items: [...] }` format for lists. **API Routes:** Return standard JSON `{ success: true, items: [...] }` format for lists.
@ -104,14 +105,12 @@ const { familyId, familyName, child, children, tier, memberCount } = useFamily()
- Model: `minimax-2.7` - Model: `minimax-2.7`
- See `/docs/debugging.md` for troubleshooting - See `/docs/debugging.md` for troubleshooting
## Authentication (Database Sessions) ## Authentication (Email + Password)
### Session Flow 1. User visits `/login` with email + password (or signs up for new account)
2. API `/api/auth/signin` verifies password hash and creates session in `sessions` table
1. User logs in at `/login` with email
2. API `/api/auth/signin` creates session in `sessions` table
3. Session token stored in **httpOnly cookie** (NOT localStorage!) 3. Session token stored in **httpOnly cookie** (NOT localStorage!)
4. On each request, session resolved from database via cookie 4. Password stored with simple hash in `users.password_hash`
### Tables Used ### Tables Used
@ -129,7 +128,7 @@ const { familyId, familyName, child, children, tier, memberCount } = useFamily()
### localStorage Acceptable For: ### localStorage Acceptable For:
- Theme preference (user-specific display only) - Theme preference (user-specific display only)
- Temporary cache (offline queue for retry) - Temporary cache (offline queue for retry)
- Chat sessions (upcoming feature: move to database) - Chat sessions local cache (synced from database)
## Admin Panel ## Admin Panel
@ -137,9 +136,9 @@ Access at: `/admin-login` (username: `admin`, password: `admin123`)
### Pages ### Pages
- `/admin` - Dashboard with stats - `/admin` - Dashboard with clickable stat cards
- `/admin/families` - Manage families - `/admin/families` - Manage families (create, view/add/remove members, set tier)
- `/admin/users` - Manage users - `/admin/users` - Manage users (add to family, password status, delete)
- `/admin/children` - Manage children - `/admin/children` - Manage children
- `/admin/revenue` - Revenue analytics - `/admin/revenue` - Revenue analytics
- `/admin/analytics` - Feature usage - `/admin/analytics` - Feature usage
@ -160,7 +159,7 @@ Access at: `/admin-login` (username: `admin`, password: `admin123`)
| Memories/Photos | Database + R2 | `/api/upload` | ✅ Yes | ✅ Yes | | Memories/Photos | Database + R2 | `/api/upload` | ✅ Yes | ✅ Yes |
| Auth Session | Database + Cookie | `/api/auth/signin` | ✅ Yes | ✅ No | | Auth Session | Database + Cookie | `/api/auth/signin` | ✅ Yes | ✅ No |
| Theme | localStorage | `tia_theme` | ✅ Yes | ✅ Yes | | Theme | localStorage | `tia_theme` | ✅ Yes | ✅ Yes |
| Chat Sessions | localStorage | `tia_chat_sessions` | ✅ Yes | ❌ No | | Chat Sessions | Database | `/api/chat` | ✅ Yes | ✅ Yes |
| Offline Queue | localStorage | `tia_offline_queue` | ✅ Yes | ❌ No | | Offline Queue | localStorage | `tia_offline_queue` | ✅ Yes | ❌ No |
## R2 Storage (Cloudflare) ## R2 Storage (Cloudflare)

2
next-env.d.ts vendored
View file

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -26,6 +26,7 @@ export default function AdminUsers() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [showAdd, setShowAdd] = useState(false); const [showAdd, setShowAdd] = useState(false);
const [showPassword, setShowPassword] = useState<string | null>(null);
const [newUser, setNewUser] = useState({ email: "", name: "", familyId: "", role: "caregiver" }); const [newUser, setNewUser] = useState({ email: "", name: "", familyId: "", role: "caregiver" });
useEffect(() => { useEffect(() => {
@ -98,6 +99,26 @@ export default function AdminUsers() {
} }
}; };
const handleSetPassword = async (userId: string, password: string) => {
if (!password) return;
try {
const res = await fetch("/api/admin/users", {
method: "PATCH",
headers: {
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, password }),
});
if (res.ok) {
fetchUsers();
setShowPassword(null);
}
} catch (err) {
console.error("Failed to set password:", err);
}
};
const filteredUsers = users.filter((u) => const filteredUsers = users.filter((u) =>
u.email.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase()) ||
(u.name || "").toLowerCase().includes(search.toLowerCase()) (u.name || "").toLowerCase().includes(search.toLowerCase())
@ -190,9 +211,19 @@ export default function AdminUsers() {
<td className="px-4 py-3">{user.familyName || "-"}</td> <td className="px-4 py-3">{user.familyName || "-"}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
{user.hasPassword ? ( {user.hasPassword ? (
<span className="text-emerald-400 text-sm"> Set</span> <button
onClick={() => setShowPassword(user.id)}
className="text-emerald-400 text-sm hover:underline"
>
Set
</button>
) : ( ) : (
<span className="text-amber-400 text-sm">Not set</span> <button
onClick={() => setShowPassword(user.id)}
className="text-amber-400 text-sm hover:underline"
>
Not set
</button>
)} )}
</td> </td>
<td className="px-4 py-3 text-sm text-gray-400"> <td className="px-4 py-3 text-sm text-gray-400">
@ -214,6 +245,38 @@ export default function AdminUsers() {
<div className="p-8 text-center text-gray-500">No users found</div> <div className="p-8 text-center text-gray-500">No users found</div>
)} )}
</div> </div>
{/* Password Modal */}
{showPassword && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-gray-800 p-6 rounded-xl w-80">
<h3 className="text-lg font-semibold mb-4">Set Password</h3>
<input
type="password"
placeholder="Enter password"
id="passwordInput"
className="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white mb-4"
/>
<div className="flex gap-2">
<button
onClick={() => {
const pwd = (document.getElementById("passwordInput") as HTMLInputElement).value;
handleSetPassword(showPassword, pwd);
}}
className="flex-1 px-4 py-2 bg-rose-500 rounded text-white"
>
Set
</button>
<button
onClick={() => setShowPassword(null)}
className="px-4 py-2 bg-gray-600 rounded text-white"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

View file

@ -87,6 +87,47 @@ export async function POST(request: Request) {
} }
} }
// Update user password or other fields
export async function PATCH(request: Request) {
try {
const authHeader = request.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { userId, password } = body;
if (!userId) {
return NextResponse.json({ error: "userId required" }, { status: 400 });
}
// Simple hash function
function hashPassword(pwd: string): string {
let hash = 0;
for (let i = 0; i < pwd.length; i++) {
const char = pwd.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return "hash_" + hash.toString(16);
}
if (password) {
const passwordHash = hashPassword(password);
await sql`
UPDATE users SET password_hash = ${passwordHash}, password_updated_at = NOW(), updated_at = NOW()
WHERE id = ${userId}
`;
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Admin password update error:", error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// Remove user from family // Remove user from family
export async function DELETE(request: Request) { export async function DELETE(request: Request) {
try { try {