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:
parent
0349be2067
commit
c65051cf7d
4 changed files with 120 additions and 17 deletions
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -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
2
next-env.d.ts
vendored
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue