Explorar el Código

feat: add admin password reset functionality

unknown hace 2 días
padre
commit
aa7cf8dcb0

+ 10 - 4
backend/routers/auth.py

@@ -181,12 +181,19 @@ async def admin_create_user(data: schemas.UserCreate, admin: dict = Depends(requ
     return user[0]
 
 @router.patch("/users/{target_id}/admin", response_model=schemas.UserResponse)
-async def admin_update_user(target_id: int, data: schemas.UserUpdate, admin: dict = Depends(require_admin)):
+async def admin_update_user(target_id: int, data: schemas.AdminUserUpdate, admin: dict = Depends(require_admin)):
     
     update_fields = []
     params = []
-    for field, value in data.dict(exclude_unset=True).items():
-        update_fields.append(f"{field} = %s")
+    update_dict = data.dict(exclude_unset=True)
+    
+    # Handle password hashing
+    if "password" in update_dict:
+        password = update_dict.pop("password")
+        update_dict["password_hash"] = auth_utils.hash_password(password)
+    
+    for field, value in update_dict.items():
+        update_fields.append(f"`{field}` = %s")
         params.append(value)
     
     if update_fields:
@@ -195,7 +202,6 @@ async def admin_update_user(target_id: int, data: schemas.UserUpdate, admin: dic
         db.execute_commit(query, tuple(params))
         
         # If user was deactivated, kick from active sessions
-        update_dict = data.dict(exclude_unset=True)
         if update_dict.get("is_active") is False:
             await global_manager.kick_user(target_id)
         

+ 4 - 0
backend/schemas.py

@@ -137,6 +137,10 @@ class UserUpdate(BaseModel):
     company_pib: Optional[str] = None
     company_address: Optional[str] = None
 
+class AdminUserUpdate(UserUpdate):
+    role: Optional[str] = None
+    password: Optional[str] = Field(None, min_length=6)
+
 class UserLogin(BaseModel):
     email: EmailStr
     password: str

BIN
backend/server_schema_dump.sql


+ 4 - 1
src/locales/en.admin.json

@@ -13,6 +13,7 @@
       "makePublic": "Make Public",
       "printInvoice": "Print Final Invoice (Faktura)",
       "printProforma": "Print Proforma (Predračun/Uplatnica)",
+      "resetPassword": "Reset Password",
       "save": "Save Changes",
       "saveChanges": "Save Changes",
       "savePrice": "Save Price",
@@ -94,7 +95,8 @@
       "editUser": "Edit User"
     },
     "questions": {
-      "deletePhoto": "Are you sure you want to delete this photo?"
+      "deletePhoto": "Are you sure you want to delete this photo?",
+      "enterNewPassword": "Enter new password for the user:"
     },
     "searchPlaceholder": "Search {tab}...",
     "searchUsersPlaceholder": "Search by name, email, phone...",
@@ -120,6 +122,7 @@
       "materialSaved": "Material saved",
       "noConsent": "User did not consent to portfolio",
       "paramsUpdated": "Parameters updated",
+      "passwordUpdated": "Password updated successfully!",
       "photoAdded": "Photo added",
       "photoDeleted": "Photo deleted successfully",
       "postDeleted": "Article deleted",

+ 5 - 1
src/locales/me.admin.json

@@ -13,6 +13,7 @@
       "makePublic": "Učini javnim",
       "printInvoice": "Odštampaj fakturu",
       "printProforma": "Odštampaj predračun",
+      "resetPassword": "Resetuj lozinku",
       "save": "Sačuvaj izmjene",
       "saveChanges": "Sačuvaj promjene",
       "savePrice": "Sačuvaj cijenu",
@@ -94,7 +95,8 @@
       "editUser": "Uredi korisnika"
     },
     "questions": {
-      "deletePhoto": "Da li ste sigurni da želite obrisati ovu fotografiju?"
+      "deletePhoto": "Da li ste sigurni da želite da obrišete ovu fotografiju?",
+      "enterNewPassword": "Unesite novu lozinku za korisnika:"
     },
     "searchPlaceholder": "Traži {tab}...",
     "searchUsersPlaceholder": "Traži po imenu, emailu, telefonu...",
@@ -120,6 +122,8 @@
       "materialSaved": "Materijal sačuvan",
       "noConsent": "Korisnik nije dao saglasnost za portfolio",
       "paramsUpdated": "Parametri ažurirani",
+      "passwordUpdated": "Lozinka uspješno ažurirana!",
+      "photoAdded": "Fotografija dodata",
       "photoAdded": "Fotografija dodata",
       "photoDeleted": "Fotografija uspješno obrisana",
       "postDeleted": "Članak obrisan",

+ 5 - 1
src/locales/ru.admin.json

@@ -13,6 +13,7 @@
       "makePublic": "Сделать публичным",
       "printInvoice": "Печать фактуры",
       "printProforma": "Печать счета на оплату",
+      "resetPassword": "Сбросить пароль",
       "save": "Сохранить",
       "saveChanges": "Сохранить изменения",
       "savePrice": "Сохранить цену",
@@ -94,7 +95,8 @@
       "editUser": "Редактировать пользователя"
     },
     "questions": {
-      "deletePhoto": "Вы уверены, что хотите удалить это фото?"
+      "deletePhoto": "Вы уверены, что хотите удалить это фото?",
+      "enterNewPassword": "Введите новый пароль для пользователя:"
     },
     "searchPlaceholder": "Поиск...",
     "searchUsersPlaceholder": "Поиск пользователей...",
@@ -120,6 +122,8 @@
       "materialSaved": "Материал сохранен",
       "noConsent": "Нет согласия",
       "paramsUpdated": "Параметры обновлены",
+      "passwordUpdated": "Пароль успешно обновлен!",
+      "photoAdded": "Фото добавлено",
       "photoAdded": "Фото добавлено",
       "photoDeleted": "Фото успешно удалено",
       "postDeleted": "Запись удалена",

+ 6 - 2
src/locales/ua.admin.json

@@ -13,7 +13,8 @@
       "makePublic": "Зробити публічним",
       "printInvoice": "Друк фактури",
       "printProforma": "Друк рахунку на оплату",
-      "save": "Зберегти",
+      "resetPassword": "Скинути пароль",
+      "save": "Зберегти зміни",
       "saveChanges": "Зберегти зміни",
       "savePrice": "Зберегти ціну",
       "sending": "Надсилання...",
@@ -94,7 +95,8 @@
       "editUser": "Редагувати користувача"
     },
     "questions": {
-      "deletePhoto": "Ви впевнені, що хочете видалити це фото?"
+      "deletePhoto": "Ви впевнені, що хочете видалити це фото?",
+      "enterNewPassword": "Введіть новий пароль для користувача:"
     },
     "searchPlaceholder": "Пошук...",
     "searchUsersPlaceholder": "Пошук користувачів...",
@@ -120,6 +122,8 @@
       "materialSaved": "Матеріал збережено",
       "noConsent": "Немає згоди",
       "paramsUpdated": "Параметри оновлено",
+      "passwordUpdated": "Пароль успішно оновлено!",
+      "photoAdded": "Фото додано",
       "photoAdded": "Фото додано",
       "photoDeleted": "Фото успішно видалено",
       "postDeleted": "Запис видалено",

+ 21 - 1
src/pages/Admin.vue

@@ -456,6 +456,11 @@
                   </td>
                   <td class="p-4 text-right">
                     <div class="flex items-center justify-end gap-2">
+                      <button @click="handleResetPassword(u.id)"
+                        class="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-primary transition-all"
+                        :title="t('admin.actions.resetPassword')">
+                        <Key class="w-4 h-4" />
+                      </button>
                       <button @click="handleUpdateUserRole(u.id, u.role === 'admin' ? 'user' : 'admin')" 
                         class="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-primary transition-all" 
                         :title="t('admin.actions.toggleAdminRole')">
@@ -910,7 +915,7 @@ import { RouterLink, useRouter, useRoute } from "vue-router";
 import { useI18n } from "vue-i18n";
 import { loadAdminTranslations } from "@/i18n";
 import { toast } from "vue-sonner";
-import { Package, Clock, CheckCircle2, Truck, XCircle, AlertCircle, FileText, ExternalLink, ShieldCheck, Eye, RefreshCw, Search, Filter, Layers, Settings, Plus, Edit2, Trash2, ToggleLeft, ToggleRight, Database, Image as ImageIcon, EyeOff, Hash, MessageCircle, FileBox, Download, Newspaper, History, X, Users, Save } from "lucide-vue-next";
+import { Package, Clock, CheckCircle2, Truck, XCircle, AlertCircle, FileText, ExternalLink, ShieldCheck, Eye, RefreshCw, Search, Filter, Layers, Settings, Plus, Edit2, Trash2, ToggleLeft, ToggleRight, Database, Image as ImageIcon, EyeOff, Hash, MessageCircle, FileBox, Download, Newspaper, History, X, Users, Save, Key } from "lucide-vue-next";
 import Button from "@/components/ui/button.vue";
 import Header from "@/components/Header.vue";
 import Footer from "@/components/Footer.vue";
@@ -1148,6 +1153,21 @@ watch(auditPage, () => {
   if (activeTab.value === 'audit') fetchAuditLogs();
 });
 
+async function handleResetPassword(userId: number) {
+  const newPass = window.prompt(t("admin.questions.enterNewPassword"));
+  if (!newPass) return;
+  if (newPass.length < 6) {
+    toast.error(t("auth.error.passwordTooShort"));
+    return;
+  }
+  try {
+    await adminUpdateUser(userId, { password: newPass });
+    toast.success(t("admin.toasts.passwordUpdated"));
+  } catch (err: any) {
+    toast.error(err.message || t("admin.toasts.genericError"));
+  }
+}
+
 watch(activeTab, fetchData, { immediate: false });
 onMounted(async () => {
   if (!authStore.user || authStore.user.role !== "admin") { router.push("/auth"); return; }