From 98dba0dece55738a9240a0a878cad8af16b56cfb Mon Sep 17 00:00:00 2001 From: Josh Krawczyk Date: Tue, 18 Aug 2020 13:00:45 -0400 Subject: [PATCH 01/12] alerts work --- api/tacticalrmm/alerts/admin.py | 5 ++- web/src/components/AlertsIcon.vue | 27 ++++++++++++---- web/src/mixins/mixins.js | 53 +++++++++++++++++++------------ 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/api/tacticalrmm/alerts/admin.py b/api/tacticalrmm/alerts/admin.py index 8c38f3f3..072d7049 100644 --- a/api/tacticalrmm/alerts/admin.py +++ b/api/tacticalrmm/alerts/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin -# Register your models here. +from .models import Alert + + +admin.site.register(Alert) diff --git a/web/src/components/AlertsIcon.vue b/web/src/components/AlertsIcon.vue index b0e2602e..3256585e 100644 --- a/web/src/components/AlertsIcon.vue +++ b/web/src/components/AlertsIcon.vue @@ -3,21 +3,33 @@ {{ alertsLengthText() }} - No Alerts + No New Alerts - {{ alert.client }} - {{ alert.hostname }} - - + {{ alert.client }} - {{ alert.site }} - {{ alert.hostname }} + + {{ alert.message }} - {{ alert.alert_time }} + {{ alertTime(alert.alert_time) }} + + + + Snooze the alert for 24 hours + + + + + Dismiss alert + + + - View All Alerts + View All Alerts ({{ alerts.length }}) @@ -72,7 +84,8 @@ export default { }, computed: { ...mapGetters({ - alerts: "alerts/getNewAlerts" + newAlerts: "alerts/getNewAlerts", + alerts: "alerts/getAlerts" }) }, mounted() { diff --git a/web/src/mixins/mixins.js b/web/src/mixins/mixins.js index 50344c0e..d181260c 100644 --- a/web/src/mixins/mixins.js +++ b/web/src/mixins/mixins.js @@ -32,30 +32,41 @@ export function notifyInfoConfig(msg, timeout = 2000) { } }; +function getTimeLapse(unixtime) { + var previous = unixtime * 1000; + var current = new Date(); + var msPerMinute = 60 * 1000; + var msPerHour = msPerMinute * 60; + var msPerDay = msPerHour * 24; + var msPerMonth = msPerDay * 30; + var msPerYear = msPerDay * 365; + var elapsed = current - previous; + if (elapsed < msPerMinute) { + return Math.round(elapsed / 1000) + " seconds ago"; + } else if (elapsed < msPerHour) { + return Math.round(elapsed / msPerMinute) + " minutes ago"; + } else if (elapsed < msPerDay) { + return Math.round(elapsed / msPerHour) + " hours ago"; + } else if (elapsed < msPerMonth) { + return Math.round(elapsed / msPerDay) + " days ago"; + } else if (elapsed < msPerYear) { + return Math.round(elapsed / msPerMonth) + " months ago"; + } else { + return Math.round(elapsed / msPerYear) + " years ago"; + } +} + export default { methods: { bootTime(unixtime) { - var previous = unixtime * 1000; - var current = new Date(); - var msPerMinute = 60 * 1000; - var msPerHour = msPerMinute * 60; - var msPerDay = msPerHour * 24; - var msPerMonth = msPerDay * 30; - var msPerYear = msPerDay * 365; - var elapsed = current - previous; - if (elapsed < msPerMinute) { - return Math.round(elapsed / 1000) + " seconds ago"; - } else if (elapsed < msPerHour) { - return Math.round(elapsed / msPerMinute) + " minutes ago"; - } else if (elapsed < msPerDay) { - return Math.round(elapsed / msPerHour) + " hours ago"; - } else if (elapsed < msPerMonth) { - return Math.round(elapsed / msPerDay) + " days ago"; - } else if (elapsed < msPerYear) { - return Math.round(elapsed / msPerMonth) + " months ago"; - } else { - return Math.round(elapsed / msPerYear) + " years ago"; - } + return getTimeLapse(unixtime) + }, + alertTime(datetime) { + console.log(datetime) + var unixtime = new Date(datetime).getTime()/1000 + console.log(unixtime) + return getTimeLapse(parseInt(unixtime)) + }, notifySuccess(msg, timeout = 2000) { Notify.create(notifySuccessConfig(msg, timeout)); From 0c71dc29b343e7630a19c35be7044ff9810809fd Mon Sep 17 00:00:00 2001 From: Josh Krawczyk Date: Wed, 19 Aug 2020 18:22:44 -0400 Subject: [PATCH 02/12] user management addition --- api/tacticalrmm/accounts/serializers.py | 38 +++ api/tacticalrmm/accounts/urls.py | 10 + api/tacticalrmm/accounts/views.py | 91 +++++ api/tacticalrmm/alerts/serializers.py | 2 + api/tacticalrmm/tacticalrmm/urls.py | 1 + web/package-lock.json | 5 + web/package.json | 1 + web/src/components/AdminManager.vue | 310 ++++++++++++++++++ web/src/components/FileBar.vue | 15 +- web/src/components/modals/admin/UserForm.vue | 137 ++++++++ .../modals/admin/UserResetPasswordForm.vue | 57 ++++ .../modals/alerts/AlertsOverview.vue | 72 ++-- web/src/mixins/mixins.js | 7 +- web/src/router/routes.js | 5 + web/src/store/admin.js | 51 +++ web/src/store/index.js | 2 + web/src/views/Login.vue | 7 +- web/src/views/TOTPSetup.vue | 76 +++++ 18 files changed, 861 insertions(+), 26 deletions(-) create mode 100644 api/tacticalrmm/accounts/serializers.py create mode 100644 api/tacticalrmm/accounts/urls.py create mode 100644 web/src/components/AdminManager.vue create mode 100644 web/src/components/modals/admin/UserForm.vue create mode 100644 web/src/components/modals/admin/UserResetPasswordForm.vue create mode 100644 web/src/store/admin.js create mode 100644 web/src/views/TOTPSetup.vue diff --git a/api/tacticalrmm/accounts/serializers.py b/api/tacticalrmm/accounts/serializers.py new file mode 100644 index 00000000..2eeb9dce --- /dev/null +++ b/api/tacticalrmm/accounts/serializers.py @@ -0,0 +1,38 @@ +import pyotp + +from rest_framework.serializers import ( + ModelSerializer, + SerializerMethodField, +) + +from .models import User + + +class UserSerializer(ModelSerializer): + + class Meta: + model = User + fields = ( + "id", + "username", + "first_name", + "last_name", + "email", + "is_active", + "last_login", + ) + +class TOTPSetupSerializer(ModelSerializer): + + qr_url = SerializerMethodField() + + class Meta: + model = User + fields = ( + "username", + "totp_key", + "qr_url", + ) + + def get_qr_url(self, obj): + return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(obj.username, issuer_name="Tactical RMM") diff --git a/api/tacticalrmm/accounts/urls.py b/api/tacticalrmm/accounts/urls.py new file mode 100644 index 00000000..703e4a2f --- /dev/null +++ b/api/tacticalrmm/accounts/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("users/", views.GetAddUsers.as_view()), + path("users//", views.GetUpdateDeleteUser.as_view()), + path("users/reset/", views.UserActions.as_view()), + path("users/reset_totp/", views.UserActions.as_view()), + path("users/setup_totp/", views.TOTPSetup.as_view()), +] diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index 1e640360..fc27dd85 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -2,7 +2,9 @@ import pyotp from django.contrib.auth import login from django.conf import settings +from django.shortcuts import get_object_or_404 +from rest_framework.views import APIView from rest_framework.authtoken.serializers import AuthTokenSerializer from knox.views import LoginView as KnoxLoginView from rest_framework.permissions import AllowAny @@ -11,6 +13,8 @@ from rest_framework import status from .models import User +from .serializers import UserSerializer, TOTPSetupSerializer + class CheckCreds(KnoxLoginView): @@ -19,6 +23,12 @@ class CheckCreds(KnoxLoginView): def post(self, request, format=None): serializer = AuthTokenSerializer(data=request.data) serializer.is_valid(raise_exception=True) + + user = User.objects.get(username=request.data["username"]) + + if not user.totp_key: + return Response("totp not set") + return Response("ok") @@ -46,3 +56,84 @@ class LoginView(KnoxLoginView): return super(LoginView, self).post(request, format=None) else: return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) + +class GetAddUsers(APIView): + def get(self, request): + users = User.objects.all() + + return Response(UserSerializer(users, many=True).data) + + def post(self, request): + + # Remove password from serializer + password = request.data.pop("password") + + serializer = UserSerializer(data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + user = serializer.save() + + # Can be changed once permissions and groups are introduced + user.is_superuser = True + user.set_password = password + user.save() + + return Response("ok") + +class GetUpdateDeleteUser(APIView): + def get(self, request, pk): + user = get_object_or_404(User, pk=pk) + + return Response(UserSerializer(user).data) + + def put(self, request, pk): + user = get_object_or_404(User, pk=pk) + + serializer = UserSerializer(instance=user, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response("ok") + + def delete(self, request, pk): + User.objects.get(pk=pk).delete() + + return Response("ok") + +class UserActions(APIView): + + # reset password + def post(self, request): + + user = User.objects.get(pk=request.data["id"]) + user.set_password(request.data["password"]) + user.save() + + return Response("ok") + + # reset two factor token + def put(self, request): + + user = User.objects.get(pk=request.data["id"]) + user.totp_key = "" + user.save() + + return Response("ok") + + +class TOTPSetup(APIView): + + permission_classes = (AllowAny,) + + # totp setup + def post(self, request): + + user = get_object_or_404(User, username=request.data["username"]) + + if not user.totp_key: + code = pyotp.random_base32() + user.totp_key = code + user.save(update_fields=["totp_key"]) + return Response(TOTPSetupSerializer(user).data) + + return Response("TOTP token already set") + diff --git a/api/tacticalrmm/alerts/serializers.py b/api/tacticalrmm/alerts/serializers.py index 09c20d9b..ab2801bb 100644 --- a/api/tacticalrmm/alerts/serializers.py +++ b/api/tacticalrmm/alerts/serializers.py @@ -1,6 +1,7 @@ from rest_framework.serializers import ( ModelSerializer, ReadOnlyField, + DateTimeField, ) from .models import Alert @@ -11,6 +12,7 @@ class AlertSerializer(ModelSerializer): hostname = ReadOnlyField(source="agent.hostname") client = ReadOnlyField(source="agent.client") site = ReadOnlyField(source="agent.site") + alert_time = DateTimeField(format="iso-8601") class Meta: model = Alert diff --git a/api/tacticalrmm/tacticalrmm/urls.py b/api/tacticalrmm/tacticalrmm/urls.py index 3b888ae4..763ef6ff 100644 --- a/api/tacticalrmm/tacticalrmm/urls.py +++ b/api/tacticalrmm/tacticalrmm/urls.py @@ -23,4 +23,5 @@ urlpatterns = [ path("logs/", include("logs.urls")), path("scripts/", include("scripts.urls")), path("alerts/", include("alerts.urls")), + path("accounts/", include("accounts.urls")) ] diff --git a/web/package-lock.json b/web/package-lock.json index 9aa4ca82..7b1911d9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15528,6 +15528,11 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true }, + "qrcode.vue": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-1.7.0.tgz", + "integrity": "sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g==" + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", diff --git a/web/package.json b/web/package.json index 695efb99..5dd59ada 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,7 @@ "@quasar/extras": "^1.9.0", "axios": "^0.19.2", "dotenv": "^8.2.0", + "qrcode.vue": "^1.7.0", "quasar": "^1.12.13" }, "devDependencies": { diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue new file mode 100644 index 00000000..bbb9a4ed --- /dev/null +++ b/web/src/components/AdminManager.vue @@ -0,0 +1,310 @@ + + + \ No newline at end of file diff --git a/web/src/components/FileBar.vue b/web/src/components/FileBar.vue index 4d6bd68a..fec15587 100644 --- a/web/src/components/FileBar.vue +++ b/web/src/components/FileBar.vue @@ -69,6 +69,10 @@ Automation Manager + + + Administration + Global Settings @@ -124,6 +128,12 @@ + +
+ + + +
@@ -142,6 +152,7 @@ import UpdateAgents from "@/components/modals/agents/UpdateAgents"; import ScriptManager from "@/components/ScriptManager"; import EditCoreSettings from "@/components/modals/coresettings/EditCoreSettings"; import AutomationManager from "@/components/automation/AutomationManager"; +import AdminManager from "@/components/AdminManager"; import InstallAgent from "@/components/modals/agents/InstallAgent"; import UploadMesh from "@/components/modals/core/UploadMesh"; @@ -158,7 +169,8 @@ export default { EditCoreSettings, AutomationManager, InstallAgent, - UploadMesh + UploadMesh, + AdminManager }, props: ["clients"], data() { @@ -170,6 +182,7 @@ export default { showUpdateAgentsModal: false, showEditCoreSettingsModal: false, showAutomationManager: false, + showAdminManager: false, showInstallAgent: false, showUploadMesh: false }; diff --git a/web/src/components/modals/admin/UserForm.vue b/web/src/components/modals/admin/UserForm.vue new file mode 100644 index 00000000..27bba518 --- /dev/null +++ b/web/src/components/modals/admin/UserForm.vue @@ -0,0 +1,137 @@ + + + \ No newline at end of file diff --git a/web/src/components/modals/admin/UserResetPasswordForm.vue b/web/src/components/modals/admin/UserResetPasswordForm.vue new file mode 100644 index 00000000..0c5c6113 --- /dev/null +++ b/web/src/components/modals/admin/UserResetPasswordForm.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertsOverview.vue b/web/src/components/modals/alerts/AlertsOverview.vue index 554ce03c..6fe517fd 100644 --- a/web/src/components/modals/alerts/AlertsOverview.vue +++ b/web/src/components/modals/alerts/AlertsOverview.vue @@ -7,25 +7,58 @@ Close - - All Alerts - - - - - {{ alert.client }} - {{ alert.hostname }} - - - {{ alert.message }} - - - - {{ alert.timestamp }} - - - - + + + +
+ + + + + +
+ +
+ +
+
+ + + + + No Alerts! + + + {{ alert.client }} - {{ alert.site }} - {{ alert.hostname }} + + + {{ alert.message }} + + + + + {{ alertTime(alert.alert_time) }} + + + + Snooze the alert for 24 hours + + + + + Dismiss alert + + + + + + @@ -38,7 +71,8 @@ export default { mixins: [mixins], data() { return { - + search: "", + includeDismissed: false }; }, methods: { diff --git a/web/src/mixins/mixins.js b/web/src/mixins/mixins.js index d181260c..d30c5428 100644 --- a/web/src/mixins/mixins.js +++ b/web/src/mixins/mixins.js @@ -59,13 +59,10 @@ function getTimeLapse(unixtime) { export default { methods: { bootTime(unixtime) { - return getTimeLapse(unixtime) + return getTimeLapse(unixtime); }, alertTime(datetime) { - console.log(datetime) - var unixtime = new Date(datetime).getTime()/1000 - console.log(unixtime) - return getTimeLapse(parseInt(unixtime)) + return getTimeLapse(Date.parse(datetime)/1000); }, notifySuccess(msg, timeout = 2000) { diff --git a/web/src/router/routes.js b/web/src/router/routes.js index 9dcb4ba8..2ea2b51a 100644 --- a/web/src/router/routes.js +++ b/web/src/router/routes.js @@ -15,6 +15,11 @@ const routes = [ requireAuth: true } }, + { + path: "/totp_setup/:username", + name: "TOTPSetup", + component: () => import("@/views/TOTPSetup") + }, { path: "/takecontrol/:pk", name: "TakeControl", diff --git a/web/src/store/admin.js b/web/src/store/admin.js new file mode 100644 index 00000000..a431be5b --- /dev/null +++ b/web/src/store/admin.js @@ -0,0 +1,51 @@ +import axios from "axios"; + +export default { + namespaced: true, + state: { + users: [] + }, + + getters: { + users(state) { + return state.checks; + } + }, + + mutations: { + setUsers(state, users) { + state.users = users; + } + }, + + actions: { + loadUsers(context) { + return axios.get("/accounts/users/").then(r => { + context.commit("setUsers", r.data); + }) + }, + loadUser(context, pk) { + return axios.get(`/accounts/users/${pk}/`); + }, + addUser(context, data) { + return axios.post("/accounts/users/", data); + }, + editUser(context, data) { + return axios.put(`/accounts/users/${data.id}/`, data); + }, + deleteUser(context, pk) { + return axios.delete(`/accounts/users/${pk}/`).then(r => { + context.dispatch("loadUsers"); + }); + }, + resetUserPassword(context, data) { + return axios.post("/accounts/users/reset/", data); + }, + resetUserTOTP(context, data) { + return axios.put("/accounts/users/reset_totp/", data); + }, + setupTOTP(context, data) { + return axios.post("/accounts/users/setup_totp/", data); + } + } +} diff --git a/web/src/store/index.js b/web/src/store/index.js index d27fb233..702dcd75 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -5,6 +5,7 @@ import { Notify } from "quasar"; import logModule from "./logs"; import alertsModule from "./alerts"; import automationModule from "./automation"; +import adminModule from "./admin.js" Vue.use(Vuex); @@ -14,6 +15,7 @@ export default function () { logs: logModule, automation: automationModule, alerts: alertsModule, + admin: adminModule }, state: { username: localStorage.getItem("user_name") || null, diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index 2938b3eb..784cb99b 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -79,7 +79,12 @@ export default { axios .post("/checkcreds/", this.credentials) .then(r => { - this.prompt = true; + if (r.data === "totp not set") { + this.$router.push({name: "TOTPSetup", params: { username: this.credentials.username }}) + } else { + + this.prompt = true; + } }) .catch(() => { this.notifyError("Bad credentials"); diff --git a/web/src/views/TOTPSetup.vue b/web/src/views/TOTPSetup.vue new file mode 100644 index 00000000..f7ed7d40 --- /dev/null +++ b/web/src/views/TOTPSetup.vue @@ -0,0 +1,76 @@ + + + From 2e2e2073fb5fc87f20e6a202ee4c98efc72c0a95 Mon Sep 17 00:00:00 2001 From: Josh Krawczyk Date: Wed, 19 Aug 2020 18:26:00 -0400 Subject: [PATCH 03/12] bump app version --- api/tacticalrmm/tacticalrmm/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index 6558f03e..7239f4f0 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -11,7 +11,7 @@ AUTH_USER_MODEL = "accounts.User" # bump this version everytime vue code is changed # to alert user they need to manually refresh their browser -APP_VER = "0.0.23" +APP_VER = "0.0.24" # https://github.com/wh1te909/salt LATEST_SALT_VER = "1.0.3" From 68c9a31f468f4efbc55a27919515784bfbe47624 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 20 Aug 2020 04:18:13 +0000 Subject: [PATCH 04/12] filter out agents from user management --- api/tacticalrmm/accounts/views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index fc27dd85..7a9c7ba6 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -12,6 +12,7 @@ from rest_framework.response import Response from rest_framework import status from .models import User +from agents.models import Agent from .serializers import UserSerializer, TOTPSetupSerializer @@ -57,9 +58,11 @@ class LoginView(KnoxLoginView): else: return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) + class GetAddUsers(APIView): def get(self, request): - users = User.objects.all() + agents = Agent.objects.values_list("agent_id", flat=True) + users = User.objects.exclude(username__in=agents) return Response(UserSerializer(users, many=True).data) @@ -79,6 +82,7 @@ class GetAddUsers(APIView): return Response("ok") + class GetUpdateDeleteUser(APIView): def get(self, request, pk): user = get_object_or_404(User, pk=pk) @@ -99,6 +103,7 @@ class GetUpdateDeleteUser(APIView): return Response("ok") + class UserActions(APIView): # reset password @@ -112,7 +117,7 @@ class UserActions(APIView): # reset two factor token def put(self, request): - + user = User.objects.get(pk=request.data["id"]) user.totp_key = "" user.save() @@ -134,6 +139,6 @@ class TOTPSetup(APIView): user.totp_key = code user.save(update_fields=["totp_key"]) return Response(TOTPSetupSerializer(user).data) - + return Response("TOTP token already set") From c3166521286d656534a4589d51e83ef99669bfe3 Mon Sep 17 00:00:00 2001 From: sadnub Date: Thu, 20 Aug 2020 09:08:41 -0600 Subject: [PATCH 05/12] Update views.py Added 404 replies if User doesn't exist --- api/tacticalrmm/accounts/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index 7a9c7ba6..a99ae5f7 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -25,7 +25,7 @@ class CheckCreds(KnoxLoginView): serializer = AuthTokenSerializer(data=request.data) serializer.is_valid(raise_exception=True) - user = User.objects.get(username=request.data["username"]) + user = get_object_or_404(User, username=request.data["username"]) if not user.totp_key: return Response("totp not set") @@ -99,7 +99,7 @@ class GetUpdateDeleteUser(APIView): return Response("ok") def delete(self, request, pk): - User.objects.get(pk=pk).delete() + get_object_or_404(User, pk=pk).delete() return Response("ok") @@ -109,7 +109,7 @@ class UserActions(APIView): # reset password def post(self, request): - user = User.objects.get(pk=request.data["id"]) + user = get_object_or_404(User, pk=request.data["id"]) user.set_password(request.data["password"]) user.save() @@ -118,7 +118,7 @@ class UserActions(APIView): # reset two factor token def put(self, request): - user = User.objects.get(pk=request.data["id"]) + user = get_object_or_404(User, pk=request.data["id"]) user.totp_key = "" user.save() From 622eb23fe00fe598512897aaeaba68ad69c5345e Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 20 Aug 2020 20:04:41 +0000 Subject: [PATCH 06/12] small fixes, highlight row when selected --- api/tacticalrmm/accounts/urls.py | 2 +- api/tacticalrmm/accounts/views.py | 21 +++++------ web/src/components/AdminManager.vue | 58 +++++++++++++---------------- web/src/store/admin.js | 6 +-- 4 files changed, 39 insertions(+), 48 deletions(-) diff --git a/api/tacticalrmm/accounts/urls.py b/api/tacticalrmm/accounts/urls.py index 703e4a2f..4ef64db2 100644 --- a/api/tacticalrmm/accounts/urls.py +++ b/api/tacticalrmm/accounts/urls.py @@ -3,7 +3,7 @@ from . import views urlpatterns = [ path("users/", views.GetAddUsers.as_view()), - path("users//", views.GetUpdateDeleteUser.as_view()), + path("/users/", views.GetUpdateDeleteUser.as_view()), path("users/reset/", views.UserActions.as_view()), path("users/reset_totp/", views.UserActions.as_view()), path("users/setup_totp/", views.TOTPSetup.as_view()), diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index 7a9c7ba6..d51a04c0 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -25,7 +25,7 @@ class CheckCreds(KnoxLoginView): serializer = AuthTokenSerializer(data=request.data) serializer.is_valid(raise_exception=True) - user = User.objects.get(username=request.data["username"]) + user = get_object_or_404(User, username=request.data["username"]) if not user.totp_key: return Response("totp not set") @@ -68,18 +68,15 @@ class GetAddUsers(APIView): def post(self, request): - # Remove password from serializer - password = request.data.pop("password") - - serializer = UserSerializer(data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - user = serializer.save() + user = User.objects.create_user( + request.data["username"], request.data["email"], request.data["password"] + ) + user.first_name = request.data["first_name"] + user.last_name = request.data["last_name"] # Can be changed once permissions and groups are introduced user.is_superuser = True - user.set_password = password user.save() - return Response("ok") @@ -99,7 +96,7 @@ class GetUpdateDeleteUser(APIView): return Response("ok") def delete(self, request, pk): - User.objects.get(pk=pk).delete() + get_object_or_404(User, pk=pk).delete() return Response("ok") @@ -109,7 +106,7 @@ class UserActions(APIView): # reset password def post(self, request): - user = User.objects.get(pk=request.data["id"]) + user = get_object_or_404(User, pk=request.data["id"]) user.set_password(request.data["password"]) user.save() @@ -118,7 +115,7 @@ class UserActions(APIView): # reset two factor token def put(self, request): - user = User.objects.get(pk=request.data["id"]) + user = get_object_or_404(User, pk=request.data["id"]) user.totp_key = "" user.save() diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue index bbb9a4ed..8a6c4264 100644 --- a/web/src/components/AdminManager.vue +++ b/web/src/components/AdminManager.vue @@ -80,7 +80,13 @@ @@ -195,24 +191,24 @@ export default { name: "name", label: "Name", field: "name", - align: "left" + align: "left", }, { name: "email", label: "Email", field: "email", - align: "left" + align: "left", }, { name: "last_login", label: "Last Login", field: "last_login", - align: "left" + align: "left", }, ], pagination: { - rowsPerPage: 9999 - } + rowsPerPage: 9999, + }, }; }, methods: { @@ -220,22 +216,22 @@ export default { this.$store.dispatch("admin/loadUsers"); }, clearRow() { - this.selected = [] + this.selected = []; }, refresh() { this.getUsers(); this.clearRow(); }, - deleteUser(id) { + deleteUser(data) { this.$q .dialog({ title: "Delete user?", cancel: true, - ok: { label: "Delete", color: "negative" } + ok: { label: "Delete", color: "negative" }, }) .onOk(() => { this.$store - .dispatch("admin/deleteUser", id) + .dispatch("admin/deleteUser", data.id) .then(response => { this.$q.notify(notifySuccessConfig("User was deleted!")); }) @@ -244,8 +240,8 @@ export default { }); }); }, - showEditUserModal(id) { - this.editUserId = id; + showEditUserModal(data) { + this.editUserId = data.id; this.showUserFormModal = true; }, closeUserFormModal() { @@ -257,12 +253,11 @@ export default { this.showUserFormModal = true; }, toggleEnabled(user) { - let text = user.is_active ? "User enabled successfully" : "User disabled successfully"; const data = { id: user.id, - is_active: user.is_active + is_active: user.is_active, }; this.$store @@ -283,9 +278,8 @@ export default { this.showResetPasswordModal = false; }, reset2FA(user) { - const data = { - id: user.id + id: user.id, }; this.$store @@ -296,15 +290,15 @@ export default { .catch(error => { this.$q.notify(notifyErrorConfig("An Error occured while resetting key")); }); - } + }, }, computed: { ...mapState({ - users: state => state.admin.users - }) + users: state => state.admin.users, + }), }, mounted() { this.refresh(); - } + }, }; \ No newline at end of file diff --git a/web/src/store/admin.js b/web/src/store/admin.js index a431be5b..6b319295 100644 --- a/web/src/store/admin.js +++ b/web/src/store/admin.js @@ -25,16 +25,16 @@ export default { }) }, loadUser(context, pk) { - return axios.get(`/accounts/users/${pk}/`); + return axios.get(`/accounts/${pk}/users/`); }, addUser(context, data) { return axios.post("/accounts/users/", data); }, editUser(context, data) { - return axios.put(`/accounts/users/${data.id}/`, data); + return axios.put(`/accounts/${data.id}/users/`, data); }, deleteUser(context, pk) { - return axios.delete(`/accounts/users/${pk}/`).then(r => { + return axios.delete(`/accounts/${pk}/users/`).then(r => { context.dispatch("loadUsers"); }); }, From a5cabba631d5dcab14c321c818f30e51e2accfad Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 20 Aug 2020 20:29:45 +0000 Subject: [PATCH 07/12] fix delete user --- web/src/components/AdminManager.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue index 8a6c4264..4ddebf49 100644 --- a/web/src/components/AdminManager.vue +++ b/web/src/components/AdminManager.vue @@ -43,7 +43,7 @@ unelevated no-caps icon="delete" - @click="deleteUser(selected[0])" + @click="deleteUser(selected[0].id)" /> { this.$store - .dispatch("admin/deleteUser", data.id) + .dispatch("admin/deleteUser", id) .then(response => { this.$q.notify(notifySuccessConfig("User was deleted!")); }) From 01ed13b05b92a1eb9d2988ca7248feb52934391e Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 20 Aug 2020 20:41:56 +0000 Subject: [PATCH 08/12] fix edit user --- web/src/components/AdminManager.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue index 4ddebf49..ba98f30f 100644 --- a/web/src/components/AdminManager.vue +++ b/web/src/components/AdminManager.vue @@ -84,7 +84,7 @@ :props="props" class="cursor-pointer" :class="{highlight: selected.length !== 0 && selected[0].id === props.row.id}" - @click="props.selected = true" + @click="editUserId = props.row.id; props.selected = true" @contextmenu="editUserId = props.row.id; props.selected = true" > @@ -93,7 +93,7 @@ @@ -148,7 +148,8 @@ {{ props.row.username }} {{ props.row.first_name }} {{ props.row.last_name }} {{ props.row.email }} - {{ props.row.last_login }} + {{ props.row.last_login }} + Never From 70d18b88bf5f52f41d7dda463ab41376bb14f3b4 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 20 Aug 2020 21:04:28 +0000 Subject: [PATCH 09/12] add email validation, hide password --- web/src/components/modals/admin/UserForm.vue | 47 +++++++++++++++---- .../modals/coresettings/EditCoreSettings.vue | 4 -- web/src/mixins/mixins.js | 8 +++- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/web/src/components/modals/admin/UserForm.vue b/web/src/components/modals/admin/UserForm.vue index 27bba518..6581add4 100644 --- a/web/src/components/modals/admin/UserForm.vue +++ b/web/src/components/modals/admin/UserForm.vue @@ -9,19 +9,46 @@
Username:
- +
Password:
- + + +
Email:
- +
@@ -63,13 +90,14 @@ export default { email: "", first_name: "", last_name: "", - is_active: true + is_active: true, + isPwd: true, }; }, computed: { title() { return this.pk ? "Edit User" : "Add User"; - } + }, }, methods: { getUser() { @@ -94,7 +122,7 @@ export default { email: this.email, is_active: this.is_active, first_name: this.first_name, - last_name: this.last_name + last_name: this.last_name, }; if (this.pk) { @@ -110,8 +138,7 @@ export default { this.$q.notify(notifyErrorConfig(e.response.data)); }); } else { - - formData.password = this.password + formData.password = this.password; this.$store .dispatch("admin/addUser", formData) @@ -125,13 +152,13 @@ export default { this.$q.notify(notifyErrorConfig(e.response.data)); }); } - } + }, }, mounted() { // If pk prop is set that means we are editting if (this.pk) { this.getUser(); } - } + }, }; \ No newline at end of file diff --git a/web/src/components/modals/coresettings/EditCoreSettings.vue b/web/src/components/modals/coresettings/EditCoreSettings.vue index e7bc258d..5c4e4fde 100644 --- a/web/src/components/modals/coresettings/EditCoreSettings.vue +++ b/web/src/components/modals/coresettings/EditCoreSettings.vue @@ -240,10 +240,6 @@ export default { const removed = this.settings.email_alert_recipients.filter(k => k !== email); this.settings.email_alert_recipients = removed; }, - isValidEmail(val) { - const email = /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/; - return email.test(val); - }, editSettings() { this.$q.loading.show(); axios diff --git a/web/src/mixins/mixins.js b/web/src/mixins/mixins.js index d30c5428..e8f6ea6c 100644 --- a/web/src/mixins/mixins.js +++ b/web/src/mixins/mixins.js @@ -62,7 +62,7 @@ export default { return getTimeLapse(unixtime); }, alertTime(datetime) { - return getTimeLapse(Date.parse(datetime)/1000); + return getTimeLapse(Date.parse(datetime) / 1000); }, notifySuccess(msg, timeout = 2000) { @@ -76,6 +76,10 @@ export default { }, notifyInfo(msg, timeout = 2000) { Notify.create(notifyInfoConfig(msg, timeout)); - } + }, + isValidEmail(val) { + const email = /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/; + return email.test(val); + }, } }; From a0dff8984237bc4018e25d531ee9cc19c644d290 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 20 Aug 2020 22:27:55 +0000 Subject: [PATCH 10/12] more improvements --- api/tacticalrmm/accounts/views.py | 8 ++-- web/src/components/AdminManager.vue | 45 +++++++++++++------ web/src/components/modals/admin/UserForm.vue | 2 +- .../modals/admin/UserResetPasswordForm.vue | 29 +++++++++--- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index d51a04c0..f30fddd9 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -67,7 +67,7 @@ class GetAddUsers(APIView): return Response(UserSerializer(users, many=True).data) def post(self, request): - + # add new user user = User.objects.create_user( request.data["username"], request.data["email"], request.data["password"] ) @@ -77,7 +77,7 @@ class GetAddUsers(APIView): # Can be changed once permissions and groups are introduced user.is_superuser = True user.save() - return Response("ok") + return Response(user.username) class GetUpdateDeleteUser(APIView): @@ -119,7 +119,9 @@ class UserActions(APIView): user.totp_key = "" user.save() - return Response("ok") + return Response( + f"{user.username}'s Two-Factor key was reset. Have them sign in again to setup" + ) class TOTPSetup(APIView): diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue index ba98f30f..e69a200b 100644 --- a/web/src/components/AdminManager.vue +++ b/web/src/components/AdminManager.vue @@ -43,7 +43,7 @@ unelevated no-caps icon="delete" - @click="deleteUser(selected[0].id)" + @click="deleteUser(selected[0])" /> @@ -163,7 +163,11 @@ - + @@ -184,6 +188,7 @@ export default { showResetPasswordModal: false, editUserId: null, resetUserId: null, + resetUserName: null, selected: [], columns: [ { name: "is_active", label: "Active", field: "is_active", align: "left" }, @@ -223,21 +228,21 @@ export default { this.getUsers(); this.clearRow(); }, - deleteUser(id) { + deleteUser(data) { this.$q .dialog({ - title: "Delete user?", + title: `Delete user ${data.username}?`, cancel: true, ok: { label: "Delete", color: "negative" }, }) .onOk(() => { this.$store - .dispatch("admin/deleteUser", id) + .dispatch("admin/deleteUser", data.id) .then(response => { - this.$q.notify(notifySuccessConfig("User was deleted!")); + this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`)); }) .catch(error => { - this.$q.notify(notifyErrorConfig("An Error occured while deleting user")); + this.$q.notify(notifyErrorConfig(`An Error occured while deleting user ${data.username}`)); }); }); }, @@ -251,6 +256,8 @@ export default { this.refresh(); }, showAddUserModal() { + this.editUserId = null; + this.selected = []; this.showUserFormModal = true; }, toggleEnabled(user) { @@ -272,10 +279,12 @@ export default { }, ResetPassword(user) { this.resetUserId = user.id; + this.resetUserName = user.username; this.showResetPasswordModal = true; }, closeResetPasswordModal(user) { this.resetUserId = null; + this.resetUserName = null; this.showResetPasswordModal = false; }, reset2FA(user) { @@ -283,13 +292,21 @@ export default { id: user.id, }; - this.$store - .dispatch("admin/resetUserTOTP", data) - .then(response => { - this.$q.notify(notifySuccessConfig("User Two-Factor key reset. Have the user sign in to setup")); + this.$q + .dialog({ + title: `Reset 2FA for ${user.username}?`, + cancel: true, + ok: { label: "Reset", color: "positive" }, }) - .catch(error => { - this.$q.notify(notifyErrorConfig("An Error occured while resetting key")); + .onOk(() => { + this.$store + .dispatch("admin/resetUserTOTP", data) + .then(response => { + this.$q.notify(notifySuccessConfig(response.data, 4000)); + }) + .catch(error => { + this.$q.notify(notifyErrorConfig("An Error occured while resetting key")); + }); }); }, }, diff --git a/web/src/components/modals/admin/UserForm.vue b/web/src/components/modals/admin/UserForm.vue index 6581add4..3c7a7844 100644 --- a/web/src/components/modals/admin/UserForm.vue +++ b/web/src/components/modals/admin/UserForm.vue @@ -145,7 +145,7 @@ export default { .then(r => { this.$q.loading.hide(); this.$emit("close"); - this.$q.notify(notifySuccessConfig("User added!")); + this.$q.notify(notifySuccessConfig(`User ${r.data} was added!`)); }) .catch(e => { this.$q.loading.hide(); diff --git a/web/src/components/modals/admin/UserResetPasswordForm.vue b/web/src/components/modals/admin/UserResetPasswordForm.vue index 0c5c6113..6d5ecc93 100644 --- a/web/src/components/modals/admin/UserResetPasswordForm.vue +++ b/web/src/components/modals/admin/UserResetPasswordForm.vue @@ -2,14 +2,28 @@ -
User Password Reset
+
{{ username }} Password Reset
New Password:
- + + +
@@ -25,10 +39,11 @@ import mixins, { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins" export default { name: "UserResetForm", mixins: [mixins], - props: { pk: Number }, + props: { pk: Number, username: String }, data() { return { - password: "" + password: "", + isPwd: true, }; }, methods: { @@ -37,7 +52,7 @@ export default { let formData = { id: this.pk, - password: this.password + password: this.password, }; this.$store @@ -51,7 +66,7 @@ export default { this.$q.loading.hide(); this.$q.notify(notifyErrorConfig(e.response.data)); }); - } - } + }, + }, }; \ No newline at end of file From 74c1d40daeca4583a7a8b8c57e52248c1cfe5989 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 20 Aug 2020 22:32:02 +0000 Subject: [PATCH 11/12] catch duplicate users --- api/tacticalrmm/accounts/views.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index f30fddd9..a85d1eb6 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -3,6 +3,7 @@ import pyotp from django.contrib.auth import login from django.conf import settings from django.shortcuts import get_object_or_404 +from django.db import IntegrityError from rest_framework.views import APIView from rest_framework.authtoken.serializers import AuthTokenSerializer @@ -13,6 +14,7 @@ from rest_framework import status from .models import User from agents.models import Agent +from tacticalrmm.utils import notify_error from .serializers import UserSerializer, TOTPSetupSerializer @@ -68,9 +70,16 @@ class GetAddUsers(APIView): def post(self, request): # add new user - user = User.objects.create_user( - request.data["username"], request.data["email"], request.data["password"] - ) + try: + user = User.objects.create_user( + request.data["username"], + request.data["email"], + request.data["password"], + ) + except IntegrityError: + return notify_error( + f"ERROR: User {request.data['username']} already exists!" + ) user.first_name = request.data["first_name"] user.last_name = request.data["last_name"] From 905d443c7d16445f885995ed1e27f9bd40a849ca Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Fri, 21 Aug 2020 04:19:34 +0000 Subject: [PATCH 12/12] finish up user management --- api/tacticalrmm/accounts/views.py | 3 +-- web/src/components/AdminManager.vue | 2 +- web/src/components/FileBar.vue | 10 +++++----- web/src/components/modals/admin/UserForm.vue | 5 ++--- .../components/modals/admin/UserResetPasswordForm.vue | 5 ++--- web/src/views/Login.vue | 9 ++++----- 6 files changed, 15 insertions(+), 19 deletions(-) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index a85d1eb6..8b9d8d35 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -26,8 +26,7 @@ class CheckCreds(KnoxLoginView): def post(self, request, format=None): serializer = AuthTokenSerializer(data=request.data) serializer.is_valid(raise_exception=True) - - user = get_object_or_404(User, username=request.data["username"]) + user = serializer.validated_data["user"] if not user.totp_key: return Response("totp not set") diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue index e69a200b..9b526e4f 100644 --- a/web/src/components/AdminManager.vue +++ b/web/src/components/AdminManager.vue @@ -2,7 +2,7 @@
- Administration + User Administration Close diff --git a/web/src/components/FileBar.vue b/web/src/components/FileBar.vue index fec15587..778589f7 100644 --- a/web/src/components/FileBar.vue +++ b/web/src/components/FileBar.vue @@ -71,7 +71,7 @@ - Administration + User Administration @@ -170,7 +170,7 @@ export default { AutomationManager, InstallAgent, UploadMesh, - AdminManager + AdminManager, }, props: ["clients"], data() { @@ -184,7 +184,7 @@ export default { showAutomationManager: false, showAdminManager: false, showInstallAgent: false, - showUploadMesh: false + showUploadMesh: false, }; }, methods: { @@ -200,7 +200,7 @@ export default { }, edited() { this.$emit("edited"); - } - } + }, + }, }; diff --git a/web/src/components/modals/admin/UserForm.vue b/web/src/components/modals/admin/UserForm.vue index 3c7a7844..e5848e52 100644 --- a/web/src/components/modals/admin/UserForm.vue +++ b/web/src/components/modals/admin/UserForm.vue @@ -1,6 +1,6 @@