feat: user management — admin create, password reset, archive/delete, quick invite
Phase 1: must_change_password enforcement + change password endpoint/page Phase 2: Admin user creation (M365-style) with temp password Phase 3: Password reset (self-service forgot + admin-triggered) Phase 4: User archive (soft delete) + hard delete with precheck Phase 5: Quick invite from admin Users page Also fixes: - Auto-create subscription for accounts missing one - Hard delete precheck ignores sole-member personal accounts - Seed script patches tree nodes for validation compliance Migrations: 031 (must_change_password), 032 (password_reset_tokens), 033 (user soft delete) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ import type {
|
||||
InviteCodeResponse,
|
||||
InviteCodeCreateRequest,
|
||||
UserDetailResponse,
|
||||
AdminUserCreate,
|
||||
AdminUserCreateResponse,
|
||||
} from '@/types/admin'
|
||||
|
||||
export const adminApi = {
|
||||
@@ -25,7 +27,9 @@ export const adminApi = {
|
||||
getDashboardActivity: () =>
|
||||
api.get<ActivityEntry[]>('/admin/dashboard/activity').then(r => r.data),
|
||||
|
||||
// Users (existing endpoints)
|
||||
// Users
|
||||
createUser: (data: AdminUserCreate) =>
|
||||
api.post<AdminUserCreateResponse>('/admin/users', data).then(r => r.data),
|
||||
listUsers: (params?: Record<string, unknown>) =>
|
||||
api.get('/admin/users', { params }).then(r => r.data),
|
||||
getUser: (id: string) =>
|
||||
@@ -41,6 +45,24 @@ export const adminApi = {
|
||||
moveUserAccount: (id: string, display_code: string) =>
|
||||
api.put(`/admin/users/${id}/move-account`, { display_code }).then(r => r.data),
|
||||
|
||||
// Users - archive & delete
|
||||
archiveUser: (id: string) =>
|
||||
api.put(`/admin/users/${id}/archive`).then(r => r.data),
|
||||
restoreUser: (id: string) =>
|
||||
api.put(`/admin/users/${id}/restore`).then(r => r.data),
|
||||
hardDeleteCheck: (id: string) =>
|
||||
api.get<{ can_delete: boolean; blockers: Record<string, number> }>(`/admin/users/${id}/hard-delete-check`).then(r => r.data),
|
||||
hardDeleteUser: (id: string) =>
|
||||
api.delete(`/admin/users/${id}/hard-delete`),
|
||||
|
||||
// Users - quick invite
|
||||
createInvite: (data: { email: string; account_display_code: string; role: string }) =>
|
||||
api.post<{ id: string; email: string; code: string; role: string; account_display_code: string; email_sent: boolean }>('/admin/invites', data).then(r => r.data),
|
||||
|
||||
// Users - password reset
|
||||
adminResetPassword: (id: string, mode: 'email_link' | 'temp_password') =>
|
||||
api.post<{ message: string; temporary_password?: string; email_sent: boolean }>(`/admin/users/${id}/password-reset`, { mode }).then(r => r.data),
|
||||
|
||||
// Users - detail + subscription
|
||||
getUserDetail: (id: string) =>
|
||||
api.get<UserDetailResponse>(`/admin/users/${id}`).then(r => r.data),
|
||||
|
||||
@@ -30,6 +30,29 @@ export const authApi = {
|
||||
async logout(): Promise<void> {
|
||||
await apiClient.post('/auth/logout')
|
||||
},
|
||||
|
||||
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
||||
await apiClient.post('/auth/password/change', {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
})
|
||||
},
|
||||
|
||||
async forgotPassword(email: string): Promise<void> {
|
||||
await apiClient.post('/auth/password/forgot', { email })
|
||||
},
|
||||
|
||||
async verifyResetToken(token: string): Promise<{ valid: boolean; email: string | null }> {
|
||||
const response = await apiClient.post<{ valid: boolean; email: string | null }>('/auth/password/verify-reset-token', { token })
|
||||
return response.data
|
||||
},
|
||||
|
||||
async resetPassword(token: string, newPassword: string): Promise<void> {
|
||||
await apiClient.post('/auth/password/reset', {
|
||||
token,
|
||||
new_password: newPassword,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default authApi
|
||||
|
||||
Reference in New Issue
Block a user