Skip to content

Commit

Permalink
Add basic user controls
Browse files Browse the repository at this point in the history
  • Loading branch information
NHAS committed Nov 21, 2024
1 parent 6734f6a commit 205d670
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 17 deletions.
13 changes: 9 additions & 4 deletions adminui/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"log"
"net/http"
"strings"

"github.com/NHAS/wag/pkg/control"
)

func (au *AdminUI) getAllRegistrationTokens(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -34,11 +36,13 @@ func (au *AdminUI) getAllRegistrationTokens(w http.ResponseWriter, r *http.Reque

func (au *AdminUI) createRegistrationToken(w http.ResponseWriter, r *http.Request) {
var (
req RegistrationTokenRequestDTO
err error
req RegistrationTokenRequestDTO
res control.RegistrationResult
err error
successMsg string
)

defer func() { au.respond(err, w) }()
defer func() { au.respondSuccess(err, successMsg, w) }()
defer r.Body.Close()
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
Expand All @@ -55,13 +59,14 @@ func (au *AdminUI) createRegistrationToken(w http.ResponseWriter, r *http.Reques
return
}

_, err = au.ctrl.NewRegistration(req.Token, req.Username, req.Overwrites, req.Uses, req.Groups...)
res, err = au.ctrl.NewRegistration(req.Token, req.Username, req.Overwrites, req.Uses, req.Groups...)
if err != nil {
log.Println("unable to create new registration token: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

successMsg = res.Token
}

func (au *AdminUI) deleteRegistrationTokens(w http.ResponseWriter, r *http.Request) {
Expand Down
1 change: 0 additions & 1 deletion adminui/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ func (au *AdminUI) editUser(w http.ResponseWriter, r *http.Request) {
err = errors.Join(errs...)

if err != nil {

w.WriteHeader(http.StatusInternalServerError)
err = fmt.Errorf("%d/%d failed to %s\n%s", len(errs), len(action.Usernames), action.Action, errors.Join(errs...).Error())
return
Expand Down
14 changes: 14 additions & 0 deletions adminui2/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,24 @@ export interface UserDTO {
mfa_type: string
groups: string[]
}


export interface UsersGetAllResponseDTO {
users: UserDTO[]
}

export enum UserEditActions {
Lock = "lock",
Unlock = "unlock",
RestMFA = "resetMFA",
}


export interface EditUsersDTO {
action: UserEditActions
usernames: string[]
}

export interface GroupDTO {
group: string
members: string[]
Expand Down
10 changes: 9 additions & 1 deletion adminui2/src/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { UserDTO } from './types'
import type { GenericResponseDTO, UserDTO, EditUsersDTO } from './types'

import { client } from '.'

export function getAllUsers(): Promise<UserDTO[]> {
return client.get('/api/management/users').then(res => res.data)
}

export function deleteUsers(users: string[]): Promise<GenericResponseDTO> {
return client.delete('/api/management/users', {data: users}).then(res => res.data)
}

export function editUser(edit: EditUsersDTO): Promise<GenericResponseDTO> {
return client.put('/api/management/users', edit).then(res => res.data)
}
2 changes: 1 addition & 1 deletion adminui2/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const policyLinks = [
const managementLinks = [
{ name: 'Registration Tokens', icon: Icons.RegistrationKey, to: '/management/registration_tokens' },
{ name: 'Users', icon: Icons.Groups, to: '/admin/users' },
{ name: 'Users', icon: Icons.Groups, to: '/management/users' },
{ name: 'Devices', icon: Icons.Device, to: '/admin/users' }
]
Expand Down
3 changes: 2 additions & 1 deletion adminui2/src/pages/RegistrationTokens.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ async function deleteToken(token: RegistrationTokenRequestDTO) {
</script>

<template>
<RegistrationToken v-model:isOpen="isCreateTokenModalOpen" v-on:success="() => {tokensStore.load(true)}"></RegistrationToken>

<main class="w-full p-4">
<RegistrationToken v-model:isOpen="isCreateTokenModalOpen" v-on:success="() => {tokensStore.load(true)}"></RegistrationToken>


<h1 class="text-4xl font-bold">Registration Tokens</h1>
<div class="mt-6 flex flex-wrap gap-6">
Expand Down
168 changes: 161 additions & 7 deletions adminui2/src/pages/Users.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,171 @@
<script setup lang="ts">
import UsersTable from '@/components/Admin/UsersTable.vue'
import { computed, ref } from 'vue'
import { useToast } from 'vue-toastification'
import PaginationControls from '@/components/PaginationControls.vue'
import { usePagination } from '@/composables/usePagination'
import { useToastError } from '@/composables/useToastError'
import { Icons } from '@/util/icons'
import ConfirmModal from '@/components/ConfirmModal.vue'
import { deleteUsers, editUser, UserEditActions, type EditUsersDTO } from '@/api'
import { useUsersStore } from '@/stores/users'
import RegistrationToken from '@/components/RegistrationToken.vue'
const usersStore = useUsersStore()
usersStore.load(false)
const filterText = ref('')
const allUsers = computed(() => usersStore.users ?? [])
const filteredUsers = computed(() => {
const arr = allUsers.value
if (filterText.value == '') {
return arr
}
const searchTerm = filterText.value.trim().toLowerCase()
return arr.filter(
x =>
x.username.toLowerCase().includes(searchTerm) ||
x.groups?.includes(searchTerm) ||
x.mfa_type?.includes(searchTerm)
)
})
const { next: nextPage, prev: prevPage, totalPages, currentItems: currentUsers, activePage } = usePagination(filteredUsers, 20)
const toast = useToast()
const { catcher } = useToastError()
async function updateUser(usernames: string[], action: UserEditActions) {
try {
let data: EditUsersDTO = {
action: action,
usernames: usernames
}
const resp = await editUser(data)
usersStore.load(true)
if (!resp.success) {
toast.error(resp.message ?? 'Failed')
return
} else {
toast.success('users ' + usernames.join(", ") + ' edited!')
}
} catch (e) {
catcher(e, 'failed to edit users: ')
}
}
async function tryDeleteUsers(rules: string[] ) {
try {
const resp = await deleteUsers(rules)
usersStore.load(true)
if (!resp.success) {
toast.error(resp.message ?? 'Failed')
return
} else {
toast.success('user ' + rules.join(", ") + ' deleted!')
}
} catch (e) {
catcher(e, 'failed to delete user: ')
}
}
const isCreateTokenModalOpen = ref(false)
</script>

<template>

<main class="w-full p-4">
<h1 class="text-4xl font-bold">User Management</h1>
<RegistrationToken v-model:isOpen="isCreateTokenModalOpen"></RegistrationToken>

<div class="mt-6 flex flex-wrap gap-6">
<div class="card w-full bg-base-100 shadow-xl">
<div class="card-body">
<UsersTable />
<h1 class="text-4xl font-bold">Users</h1>
<div class="mt-6 flex flex-wrap gap-6">
<div class="card w-full bg-base-100 shadow-xl min-w-[800px]">
<div class="card-body">
<div class="flex flex-row justify-between">
<div class="tooltip" data-tip="Create Registration Token">
<button class="btn btn-ghost btn-primary" @click="isCreateTokenModalOpen = true">Add User <font-awesome-icon
:icon="Icons.Add" /></button>
</div>
<div class="form-control">
<label class="label">
<input type="text" class="input input-bordered input-sm" placeholder="Filter..."
v-model="filterText" />
</label>
</div>
</div>

<table class="table table-fixed w-full">
<thead>
<tr>
<th>Username</th>
<th>Groups</th>
<th>Devices</th>
<th>MFA Method</th>
<th>Locked</th>
</tr>
</thead>
<tbody>
<tr class="hover group" v-for="user in currentUsers" :key="user.username">
<td class="font-mono">
<div class="overflow-hidden text-ellipsis whitespace-nowrap">{{ user.username }}</div>
</td>
<td class="font-mono">
<div class="overflow-hidden text-ellipsis whitespace-nowrap">{{ user.groups?.join(", ") }}</div>
</td>
<td class="font-mono">
<div class="overflow-hidden text-ellipsis whitespace-nowrap">{{ user.devices }}</div>
</td>
<td class="font-mono">
<div class="overflow-hidden text-ellipsis whitespace-nowrap">{{ user.mfa_type }}</div>
</td>
<td class="font-mono relative">
<div><font-awesome-icon class="cursor-pointer" @click="updateUser([user.username], (user.locked) ? UserEditActions.Unlock : UserEditActions.Lock)" :icon="user.locked ? Icons.Locked : Icons.Unlocked" :class="user.locked ? 'text-error' : 'text-secondary'" /></div>
<div v-if="user.mfa_type != 'unset'" class="mr-3 absolute right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div class="tooltip" data-tip="Reset MFA">
<button class="mr-3" @click="updateUser([user.username], UserEditActions.Lock)">
<font-awesome-icon :icon="Icons.Refresh" class="text-secondary hover:text-secondary-focus" />
</button>
</div>
</div>
<ConfirmModal @on-confirm="() => tryDeleteUsers([user.username])">
<button class="absolute right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<font-awesome-icon :icon="Icons.Delete" class="text-error hover:text-error-focus" />
</button>
</ConfirmModal>
</td>
</tr>
</tbody>
</table>

<div class="mt-2 w-full text-center">
<PaginationControls @next="() => nextPage()" @prev="() => prevPage()" :current-page="activePage"
:total-pages="totalPages" />
</div>
</div>
</div>
</div>
</div>
</main>
</template>

<style scoped>
.hashlist-table.table-sm :where(th, td) {
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
</style>
2 changes: 1 addition & 1 deletion adminui2/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const router = createRouter({
// route('/diagnostics/check', 'Check Firewall', () => import('@/pages/projects/project.vue')),
// route('/diagnostics/acls', 'ACLs', () => import('@/pages/projects/project.vue')),

// route('/management/users', 'User Management', () => import('@/pages/Hashlist.vue')),
route('/management/users', 'User Management', () => import('@/pages/Users.vue')),
// route('/management/devices', 'Device Management', () => import('@/pages/Hashlist.vue')),
route('/management/registration_tokens', 'Registration Tokens', () => import('@/pages/RegistrationTokens.vue')),

Expand Down
5 changes: 4 additions & 1 deletion adminui2/src/util/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ export const Icons = {
Add: 'fa-solid fa-plus-circle',
Info: 'fa-solid fa-circle-info',
Clipboard: 'fa-solid fa-clipboard',
Refresh: 'fa-solid fa-refresh',

// objects
Agent: 'fa-solid fa-robot',
User: 'fa-solid fa-user',
RegistrationKey: 'fa-solid fa-key',

Open: 'fa-folid fa-bars',
Open: 'fa-solid fa-bars',

Listfile: 'fa-solid fa-file-lines',

Expand Down Expand Up @@ -55,6 +56,8 @@ export const Icons = {
RandomlyGenerated: 'fa-solid fa-dice',
Tick: 'fa-solid fa-check',
Locked: 'fa-solid fa-lock',
Unlocked: 'fa-solid fa-unlock',

Awaiting: 'fa-solid fa-hourglass-end',
Dead: 'fa-solid fa-skull-crossbones',
Unknown: 'fa-solid fa-question'
Expand Down

0 comments on commit 205d670

Please sign in to comment.