Merge pull request #56 from sadnub/feature-policies-alerts
User Management addition
This commit is contained in:
commit
098960009d
|
@ -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")
|
|
@ -0,0 +1,10 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", views.GetAddUsers.as_view()),
|
||||
path("<int:pk>/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()),
|
||||
]
|
|
@ -2,7 +2,10 @@ 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
|
||||
from knox.views import LoginView as KnoxLoginView
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
@ -10,6 +13,10 @@ from rest_framework.response import Response
|
|||
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
|
||||
|
||||
|
||||
class CheckCreds(KnoxLoginView):
|
||||
|
@ -19,6 +26,11 @@ class CheckCreds(KnoxLoginView):
|
|||
def post(self, request, format=None):
|
||||
serializer = AuthTokenSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data["user"]
|
||||
|
||||
if not user.totp_key:
|
||||
return Response("totp not set")
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
|
@ -46,3 +58,94 @@ 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):
|
||||
agents = Agent.objects.values_list("agent_id", flat=True)
|
||||
users = User.objects.exclude(username__in=agents)
|
||||
|
||||
return Response(UserSerializer(users, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
# add new user
|
||||
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"]
|
||||
# Can be changed once permissions and groups are introduced
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
return Response(user.username)
|
||||
|
||||
|
||||
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):
|
||||
get_object_or_404(User, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class UserActions(APIView):
|
||||
|
||||
# reset password
|
||||
def post(self, request):
|
||||
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
user.set_password(request.data["password"])
|
||||
user.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
# reset two factor token
|
||||
def put(self, request):
|
||||
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
user.totp_key = ""
|
||||
user.save()
|
||||
|
||||
return Response(
|
||||
f"{user.username}'s Two-Factor key was reset. Have them sign in again to setup"
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from .models import Alert
|
||||
|
||||
|
||||
admin.site.register(Alert)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"))
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
<template>
|
||||
<div style="width: 900px; max-width: 90vw;">
|
||||
<q-card>
|
||||
<q-bar>
|
||||
<q-btn ref="refresh" @click="refresh" class="q-mr-sm" dense flat push icon="refresh" />User Administration
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn
|
||||
ref="new"
|
||||
label="New"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="showAddUserModal"
|
||||
/>
|
||||
<q-btn
|
||||
ref="edit"
|
||||
label="Edit"
|
||||
:disable="selected.length === 0"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="edit"
|
||||
@click="showEditUserModal(selected[0])"
|
||||
/>
|
||||
<q-btn
|
||||
ref="delete"
|
||||
label="Delete"
|
||||
:disable="selected.length === 0"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="delete"
|
||||
@click="deleteUser(selected[0])"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
:data="users"
|
||||
:columns="columns"
|
||||
:pagination.sync="pagination"
|
||||
:selected.sync="selected"
|
||||
selection="single"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-pagination
|
||||
:hide-bottom="!!selected"
|
||||
>
|
||||
<!-- header slots -->
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<template v-for="col in props.cols">
|
||||
<q-th v-if="col.name === 'active'" auto-width :key="col.name">
|
||||
<q-icon name="power_settings_new" size="1.5em">
|
||||
<q-tooltip>Enable User</q-tooltip>
|
||||
</q-icon>
|
||||
</q-th>
|
||||
|
||||
<q-th v-else :key="col.name" :props="props">{{ col.label }}</q-th>
|
||||
</template>
|
||||
</q-tr>
|
||||
</template>
|
||||
<!-- No data Slot -->
|
||||
<template v-slot:no-data>
|
||||
<div class="full-width row flex-center q-gutter-sm">
|
||||
<span v-if="users.length === 0">No Users</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
:class="{highlight: selected.length !== 0 && selected[0].id === props.row.id}"
|
||||
@click="editUserId = props.row.id; props.selected = true"
|
||||
@contextmenu="editUserId = props.row.id; props.selected = true"
|
||||
>
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="showEditUserModal(selected[0])"
|
||||
id="context-edit"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteUser(props.row)"
|
||||
id="context-delete"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
</q-item-section>
|
||||
<q-item-section>Delete</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="ResetPassword(props.row)"
|
||||
id="context-reset"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="autorenew" />
|
||||
</q-item-section>
|
||||
<q-item-section>Reset Password</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="reset2FA(props.row)" id="context-reset">
|
||||
<q-item-section side>
|
||||
<q-icon name="autorenew" />
|
||||
</q-item-section>
|
||||
<q-item-section>Reset Two-Factor Auth</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Close</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<!-- enabled checkbox -->
|
||||
<q-td>
|
||||
<q-checkbox dense @input="toggleEnabled(props.row)" v-model="props.row.is_active" />
|
||||
</q-td>
|
||||
<q-td>{{ props.row.username }}</q-td>
|
||||
<q-td>{{ props.row.first_name }} {{ props.row.last_name }}</q-td>
|
||||
<q-td>{{ props.row.email }}</q-td>
|
||||
<q-td v-if="props.row.last_login">{{ props.row.last_login }}</q-td>
|
||||
<q-td v-else>Never</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<!-- user form modal -->
|
||||
<q-dialog v-model="showUserFormModal" @hide="closeUserFormModal">
|
||||
<UserForm :pk="editUserId" @close="closeUserFormModal" />
|
||||
</q-dialog>
|
||||
|
||||
<!-- user reset password form modal -->
|
||||
<q-dialog v-model="showResetPasswordModal" @hide="closeResetPasswordModal">
|
||||
<UserResetPasswordForm
|
||||
:pk="resetUserId"
|
||||
:username="resetUserName"
|
||||
@close="closeResetPasswordModal"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins, { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
|
||||
import { mapState } from "vuex";
|
||||
import UserForm from "@/components/modals/admin/UserForm";
|
||||
import UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm";
|
||||
|
||||
export default {
|
||||
name: "AdminManager",
|
||||
components: { UserForm, UserResetPasswordForm },
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
showUserFormModal: false,
|
||||
showResetPasswordModal: false,
|
||||
editUserId: null,
|
||||
resetUserId: null,
|
||||
resetUserName: null,
|
||||
selected: [],
|
||||
columns: [
|
||||
{ name: "is_active", label: "Active", field: "is_active", align: "left" },
|
||||
{ name: "username", label: "Username", field: "username", align: "left" },
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
label: "Email",
|
||||
field: "email",
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
name: "last_login",
|
||||
label: "Last Login",
|
||||
field: "last_login",
|
||||
align: "left",
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 9999,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getUsers() {
|
||||
this.$store.dispatch("admin/loadUsers");
|
||||
},
|
||||
clearRow() {
|
||||
this.selected = [];
|
||||
},
|
||||
refresh() {
|
||||
this.getUsers();
|
||||
this.clearRow();
|
||||
},
|
||||
deleteUser(data) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: `Delete user ${data.username}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$store
|
||||
.dispatch("admin/deleteUser", data.id)
|
||||
.then(response => {
|
||||
this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`));
|
||||
})
|
||||
.catch(error => {
|
||||
this.$q.notify(notifyErrorConfig(`An Error occured while deleting user ${data.username}`));
|
||||
});
|
||||
});
|
||||
},
|
||||
showEditUserModal(data) {
|
||||
this.editUserId = data.id;
|
||||
this.showUserFormModal = true;
|
||||
},
|
||||
closeUserFormModal() {
|
||||
this.showUserFormModal = false;
|
||||
this.editUserId = null;
|
||||
this.refresh();
|
||||
},
|
||||
showAddUserModal() {
|
||||
this.editUserId = null;
|
||||
this.selected = [];
|
||||
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,
|
||||
};
|
||||
|
||||
this.$store
|
||||
.dispatch("admin/editUser", data)
|
||||
.then(response => {
|
||||
this.$q.notify(notifySuccessConfig(text));
|
||||
})
|
||||
.catch(error => {
|
||||
this.$q.notify(notifyErrorConfig("An Error occured while editing user"));
|
||||
});
|
||||
},
|
||||
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) {
|
||||
const data = {
|
||||
id: user.id,
|
||||
};
|
||||
|
||||
this.$q
|
||||
.dialog({
|
||||
title: `Reset 2FA for ${user.username}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Reset", color: "positive" },
|
||||
})
|
||||
.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"));
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
users: state => state.admin.users,
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.refresh();
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -3,21 +3,33 @@
|
|||
<q-badge v-if="alerts.length !== 0" color="red" floating transparent>{{ alertsLengthText() }}</q-badge>
|
||||
<q-menu>
|
||||
<q-list separator>
|
||||
<q-item v-if="alerts.length === 0">No Alerts</q-item>
|
||||
<q-item v-if="alerts.length === 0">No New Alerts</q-item>
|
||||
<q-item v-for="alert in alerts" :key="alert.id">
|
||||
<q-item-section>
|
||||
<q-item-label>{{ alert.client }} - {{ alert.hostname }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
<q-icon :class="`text-${alertColor(alert.severity)}`" :name="alert.severity"></q-icon>
|
||||
<q-item-label overline>{{ alert.client }} - {{ alert.site }} - {{ alert.hostname }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-icon size="xs" :class="`text-${alertColor(alert.severity)}`" :name="alert.severity"></q-icon>
|
||||
{{ alert.message }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side top>
|
||||
<q-item-label caption>{{ alert.alert_time }}</q-item-label>
|
||||
<q-item-label caption>{{ alertTime(alert.alert_time) }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-icon name="snooze" size="xs">
|
||||
<q-tooltip>
|
||||
Snooze the alert for 24 hours
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon name="alarm_off" size="xs">
|
||||
<q-tooltip>
|
||||
Dismiss alert
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="showAlertsModal = true">View All Alerts</q-item>
|
||||
<q-item clickable @click="showAlertsModal = true">View All Alerts ({{ alerts.length }})</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
|
||||
|
@ -72,7 +84,8 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
alerts: "alerts/getNewAlerts"
|
||||
newAlerts: "alerts/getNewAlerts",
|
||||
alerts: "alerts/getAlerts"
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -69,6 +69,10 @@
|
|||
<q-item clickable v-close-popup @click="showAutomationManager = true">
|
||||
<q-item-section>Automation Manager</q-item-section>
|
||||
</q-item>
|
||||
<!-- admin manager -->
|
||||
<q-item clickable v-close-popup @click="showAdminManager = true">
|
||||
<q-item-section>User Administration</q-item-section>
|
||||
</q-item>
|
||||
<!-- core settings -->
|
||||
<q-item clickable v-close-popup @click="showEditCoreSettingsModal = true">
|
||||
<q-item-section>Global Settings</q-item-section>
|
||||
|
@ -124,6 +128,12 @@
|
|||
<AutomationManager @close="showAutomationManager = false" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
<!-- Admin Manager -->
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-dialog v-model="showAdminManager">
|
||||
<AdminManager @close="showAdminManager = false" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
<!-- Upload new mesh agent -->
|
||||
<q-dialog v-model="showUploadMesh">
|
||||
<UploadMesh @close="showUploadMesh = false" />
|
||||
|
@ -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,8 +182,9 @@ export default {
|
|||
showUpdateAgentsModal: false,
|
||||
showEditCoreSettingsModal: false,
|
||||
showAutomationManager: false,
|
||||
showAdminManager: false,
|
||||
showInstallAgent: false,
|
||||
showUploadMesh: false
|
||||
showUploadMesh: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -187,7 +200,7 @@ export default {
|
|||
},
|
||||
edited() {
|
||||
this.$emit("edited");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<q-card style="width: 60vw">
|
||||
<q-form ref="form" @submit="onSubmit">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">{{ title }}</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Username:</div>
|
||||
<div class="col-10">
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="username"
|
||||
:rules="[ val => !!val || '*Required']"
|
||||
class="q-pa-none"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row" v-if="!this.pk">
|
||||
<div class="col-2">Password:</div>
|
||||
<div class="col-10">
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="password"
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:rules="[ val => !!val || '*Required']"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Email:</div>
|
||||
<div class="col-10">
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="email"
|
||||
:rules="[val => isValidEmail(val) || 'Invalid email']"
|
||||
class="q-pa-none"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">First Name:</div>
|
||||
<div class="col-10">
|
||||
<q-input outlined dense v-model="first_name" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Last Name:</div>
|
||||
<div class="col-10">
|
||||
<q-input outlined dense v-model="last_name" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Active:</div>
|
||||
<div class="col-10">
|
||||
<q-toggle v-model="is_active" color="green" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row items-center">
|
||||
<q-btn :label="title" color="primary" type="submit" />
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins, { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
name: "UserForm",
|
||||
mixins: [mixins],
|
||||
props: { pk: Number },
|
||||
data() {
|
||||
return {
|
||||
username: "",
|
||||
password: "",
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
is_active: true,
|
||||
isPwd: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.pk ? "Edit User" : "Add User";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getUser() {
|
||||
this.$q.loading.show();
|
||||
|
||||
this.$store.dispatch("admin/loadUser", this.pk).then(r => {
|
||||
this.$q.loading.hide();
|
||||
|
||||
this.username = r.data.username;
|
||||
this.email = r.data.email;
|
||||
this.is_active = r.data.is_active;
|
||||
this.first_name = r.data.first_name;
|
||||
this.last_name = r.data.last_name;
|
||||
});
|
||||
},
|
||||
onSubmit() {
|
||||
this.$q.loading.show();
|
||||
let formData = {
|
||||
id: this.pk,
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
is_active: this.is_active,
|
||||
first_name: this.first_name,
|
||||
last_name: this.last_name,
|
||||
};
|
||||
|
||||
if (this.pk) {
|
||||
this.$store
|
||||
.dispatch("admin/editUser", formData)
|
||||
.then(r => {
|
||||
this.$q.loading.hide();
|
||||
this.$emit("close");
|
||||
this.$q.notify(notifySuccessConfig("User edited!"));
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
this.$q.notify(notifyErrorConfig(e.response.data));
|
||||
});
|
||||
} else {
|
||||
formData.password = this.password;
|
||||
|
||||
this.$store
|
||||
.dispatch("admin/addUser", formData)
|
||||
.then(r => {
|
||||
this.$q.loading.hide();
|
||||
this.$emit("close");
|
||||
this.$q.notify(notifySuccessConfig(`User ${r.data} was added!`));
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
this.$q.notify(notifyErrorConfig(e.response.data));
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// If pk prop is set that means we are editting
|
||||
if (this.pk) {
|
||||
this.getUser();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<q-card style="width: 60vw">
|
||||
<q-form ref="form" @submit="onSubmit">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">{{ username }} Password Reset</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">New Password:</div>
|
||||
<div class="col-10">
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="password"
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:rules="[ val => !!val || '*Required']"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row items-center">
|
||||
<q-btn label="Reset" color="primary" type="submit" />
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins, { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
name: "UserResetForm",
|
||||
mixins: [mixins],
|
||||
props: { pk: Number, username: String },
|
||||
data() {
|
||||
return {
|
||||
password: "",
|
||||
isPwd: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$q.loading.show();
|
||||
let formData = {
|
||||
id: this.pk,
|
||||
password: this.password,
|
||||
};
|
||||
|
||||
this.$store
|
||||
.dispatch("admin/resetUserPassword", formData)
|
||||
.then(r => {
|
||||
this.$q.loading.hide();
|
||||
this.$emit("close");
|
||||
this.$q.notify(notifySuccessConfig("User Password Reset!"));
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
this.$q.notify(notifyErrorConfig(e.response.data));
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -7,25 +7,58 @@
|
|||
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-separator />
|
||||
<q-card-section>All Alerts</q-card-section>
|
||||
<q-card-section>
|
||||
<q-list separator>
|
||||
<q-item v-for="alert in alerts" :key="alert.id">
|
||||
<q-item-section>
|
||||
<q-item-label>{{ alert.client }} - {{ alert.hostname }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
<q-icon :class="`text-${alertColor(alert.severity)}`" :name="alert.severity"></q-icon>
|
||||
{{ alert.message }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side top>
|
||||
<q-item-label caption>{{ alert.timestamp }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="row">
|
||||
<div class="col-3">
|
||||
<q-input outlined dense v-model="search">
|
||||
<template v-slot:append>
|
||||
<q-icon v-if="search !== ''" name="close" @click="search = ''" class="cursor-pointer" />
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
|
||||
<template v-slot:hint>
|
||||
Type in client, site, or agent name
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<q-checkbox outlined dense v-model="includeDismissed" label="Include dismissed alerts?"/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-list separator>
|
||||
<q-item v-if="alerts.length === 0">No Alerts!</q-item>
|
||||
<q-item v-for="alert in alerts" :key="alert.id">
|
||||
<q-item-section>
|
||||
<q-item-label overline>{{ alert.client }} - {{ alert.site }} - {{ alert.hostname }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-icon size="sm" :class="`text-${alertColor(alert.severity)}`" :name="alert.severity"></q-icon>
|
||||
{{ alert.message }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side top>
|
||||
<q-item-label caption>{{ alertTime(alert.alert_time) }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-icon name="snooze" size="sm">
|
||||
<q-tooltip>
|
||||
Snooze the alert for 24 hours
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon name="alarm_off" size="sm">
|
||||
<q-tooltip>
|
||||
Dismiss alert
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
|
@ -38,7 +71,8 @@ export default {
|
|||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
|
||||
search: "",
|
||||
includeDismissed: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,30 +32,38 @@ 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) {
|
||||
return getTimeLapse(Date.parse(datetime) / 1000);
|
||||
|
||||
},
|
||||
notifySuccess(msg, timeout = 2000) {
|
||||
Notify.create(notifySuccessConfig(msg, timeout));
|
||||
|
@ -68,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);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -15,6 +15,11 @@ const routes = [
|
|||
requireAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/totp_setup/:username",
|
||||
name: "TOTPSetup",
|
||||
component: () => import("@/views/TOTPSetup")
|
||||
},
|
||||
{
|
||||
path: "/takecontrol/:pk",
|
||||
name: "TakeControl",
|
||||
|
|
|
@ -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/${pk}/users/`);
|
||||
},
|
||||
addUser(context, data) {
|
||||
return axios.post("/accounts/users/", data);
|
||||
},
|
||||
editUser(context, data) {
|
||||
return axios.put(`/accounts/${data.id}/users/`, data);
|
||||
},
|
||||
deleteUser(context, pk) {
|
||||
return axios.delete(`/accounts/${pk}/users/`).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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
credentials: {},
|
||||
prompt: false
|
||||
prompt: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -79,7 +79,11 @@ 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");
|
||||
|
@ -97,11 +101,11 @@ export default {
|
|||
this.credentials = {};
|
||||
this.prompt = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$q.dark.set(true);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div class="q-pa-md">
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Setup 2-Factor</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<p> Scan the QR Code with your authenticator app and then click
|
||||
Finish to be redirected back to the signin page.</p>
|
||||
<qrcode-vue :value="qr_url" size="200" level="H" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<p> You can also use the below code to configure the authenticator manually.</p>
|
||||
<p>{{ totp_key }}</p>
|
||||
</q-card-section>
|
||||
<q-card-actions align="center">
|
||||
<q-btn label="Finish" color="primary" class="full-width" @click="finish" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
|
||||
export default {
|
||||
name: "TOTPSetup",
|
||||
components: { QrcodeVue },
|
||||
data() {
|
||||
return {
|
||||
totp_key: "",
|
||||
qr_url: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getQRCodeData() {
|
||||
this.$q.loading = true;
|
||||
|
||||
const data = {
|
||||
username: this.$route.params.username
|
||||
};
|
||||
|
||||
this.$store
|
||||
.dispatch("admin/setupTOTP", data)
|
||||
.then(r => {
|
||||
this.$q.loading = false;
|
||||
|
||||
if (r.data === "TOTP token already set") {
|
||||
this.$router.push({ name: "Login" });
|
||||
} else {
|
||||
this.totp_key = r.data.totp_key;
|
||||
this.qr_url = r.data.qr_url;
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading = false;
|
||||
console.log(e.response);
|
||||
|
||||
});
|
||||
},
|
||||
finish() {
|
||||
this.$router.push({ name: "Login" });
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$q.dark.set(false);
|
||||
this.getQRCodeData();
|
||||
}
|
||||
};
|
||||
</script>
|
Loading…
Reference in New Issue