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
|
||||
│ ├── onboarding/ # First-time setup
|
||||
│ ├── settings/ # Settings with theme picker
|
||||
│ ├── login/ # User login (magic)
|
||||
│ ├── login/ # User login (email + password)
|
||||
│ ├── admin/ # Admin panel
|
||||
│ └── admin-login/ # Admin login (separate)
|
||||
├── ThemeProvider.tsx # Theme context (light/dark/system/time)
|
||||
|
|
@ -67,6 +67,7 @@ docs/ # Design docs
|
|||
- **Vaccinations:** IAP schedule tracking
|
||||
- **Growth:** Weight/height over time
|
||||
- **Memories:** Photos with R2 storage
|
||||
- **Chat Sessions:** User conversations with AI (chat_sessions, chat_messages)
|
||||
|
||||
### 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.
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -104,14 +105,12 @@ const { familyId, familyName, child, children, tier, memberCount } = useFamily()
|
|||
- Model: `minimax-2.7`
|
||||
- See `/docs/debugging.md` for troubleshooting
|
||||
|
||||
## Authentication (Database Sessions)
|
||||
## Authentication (Email + Password)
|
||||
|
||||
### Session Flow
|
||||
|
||||
1. User logs in at `/login` with email
|
||||
2. API `/api/auth/signin` creates session in `sessions` table
|
||||
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
|
||||
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
|
||||
|
||||
|
|
@ -129,7 +128,7 @@ const { familyId, familyName, child, children, tier, memberCount } = useFamily()
|
|||
### localStorage Acceptable For:
|
||||
- Theme preference (user-specific display only)
|
||||
- Temporary cache (offline queue for retry)
|
||||
- Chat sessions (upcoming feature: move to database)
|
||||
- Chat sessions local cache (synced from database)
|
||||
|
||||
## Admin Panel
|
||||
|
||||
|
|
@ -137,9 +136,9 @@ Access at: `/admin-login` (username: `admin`, password: `admin123`)
|
|||
|
||||
### Pages
|
||||
|
||||
- `/admin` - Dashboard with stats
|
||||
- `/admin/families` - Manage families
|
||||
- `/admin/users` - Manage users
|
||||
- `/admin` - Dashboard with clickable stat cards
|
||||
- `/admin/families` - Manage families (create, view/add/remove members, set tier)
|
||||
- `/admin/users` - Manage users (add to family, password status, delete)
|
||||
- `/admin/children` - Manage children
|
||||
- `/admin/revenue` - Revenue analytics
|
||||
- `/admin/analytics` - Feature usage
|
||||
|
|
@ -160,7 +159,7 @@ Access at: `/admin-login` (username: `admin`, password: `admin123`)
|
|||
| Memories/Photos | Database + R2 | `/api/upload` | ✅ Yes | ✅ Yes |
|
||||
| Auth Session | Database + Cookie | `/api/auth/signin` | ✅ Yes | ✅ No |
|
||||
| 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 |
|
||||
|
||||
## 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/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// 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 [search, setSearch] = useState("");
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState<string | null>(null);
|
||||
const [newUser, setNewUser] = useState({ email: "", name: "", familyId: "", role: "caregiver" });
|
||||
|
||||
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) =>
|
||||
u.email.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.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 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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue