user management addition
This commit is contained in:
parent
98dba0dece
commit
0c71dc29b3
|
@ -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("users/<int:pk>/", 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,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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,310 @@
|
|||
<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" />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" @click="props.selected = true">
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="showEditUserModal(props.row.id)"
|
||||
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)"
|
||||
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>{{ props.row.last_login }}</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" @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,
|
||||
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(id) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Delete user?",
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" }
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$store
|
||||
.dispatch("admin/deleteUser", id)
|
||||
.then(response => {
|
||||
this.$q.notify(notifySuccessConfig("User was deleted!"));
|
||||
})
|
||||
.catch(error => {
|
||||
this.$q.notify(notifyErrorConfig("An Error occured while deleting user"));
|
||||
});
|
||||
});
|
||||
},
|
||||
showEditUserModal(id) {
|
||||
this.editUserId = id;
|
||||
this.showUserFormModal = true;
|
||||
},
|
||||
closeUserFormModal() {
|
||||
this.showUserFormModal = false;
|
||||
this.editUserId = null;
|
||||
this.refresh();
|
||||
},
|
||||
showAddUserModal() {
|
||||
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.showResetPasswordModal = true;
|
||||
},
|
||||
closeResetPasswordModal(user) {
|
||||
this.resetUserId = null;
|
||||
this.showResetPasswordModal = false;
|
||||
},
|
||||
reset2FA(user) {
|
||||
|
||||
const data = {
|
||||
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"));
|
||||
})
|
||||
.catch(error => {
|
||||
this.$q.notify(notifyErrorConfig("An Error occured while resetting key"));
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
users: state => state.admin.users
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
this.refresh();
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -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>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,6 +182,7 @@ export default {
|
|||
showUpdateAgentsModal: false,
|
||||
showEditCoreSettingsModal: false,
|
||||
showAutomationManager: false,
|
||||
showAdminManager: false,
|
||||
showInstallAgent: false,
|
||||
showUploadMesh: false
|
||||
};
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<q-card style="width: 60vw">
|
||||
<q-form ref="form" @submit="submit">
|
||||
<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']" />
|
||||
</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" :rules="[ val => !!val || '*Required']" />
|
||||
</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 => !!val || '*Required']" />
|
||||
</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
|
||||
};
|
||||
},
|
||||
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;
|
||||
});
|
||||
},
|
||||
submit() {
|
||||
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 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,57 @@
|
|||
<template>
|
||||
<q-card style="width: 60vw">
|
||||
<q-form ref="form" @submit="submit">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">User 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" :rules="[ val => !!val || '*Required']" />
|
||||
</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 },
|
||||
data() {
|
||||
return {
|
||||
password: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
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: {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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/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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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