WIP - more admin users and roles
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Param,
|
Param,
|
||||||
Body,
|
Body,
|
||||||
@@ -53,6 +54,46 @@ export class SetupRolesController {
|
|||||||
return role;
|
return role;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
async updateRole(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() data: { name?: string; description?: string; guardName?: string },
|
||||||
|
) {
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
const updateData: any = {};
|
||||||
|
|
||||||
|
if (data.name) updateData.name = data.name;
|
||||||
|
if (data.description !== undefined) updateData.description = data.description;
|
||||||
|
if (data.guardName) updateData.guardName = data.guardName;
|
||||||
|
|
||||||
|
const role = await Role.query(knex).patchAndFetchById(id, updateData);
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteRole(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
) {
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
// Delete role user assignments first
|
||||||
|
await knex('user_roles').where({ roleId: id }).delete();
|
||||||
|
|
||||||
|
// Delete role permissions
|
||||||
|
await knex('role_permissions').where({ roleId: id }).delete();
|
||||||
|
await knex('role_object_permissions').where({ roleId: id }).delete();
|
||||||
|
|
||||||
|
// Delete the role
|
||||||
|
await Role.query(knex).deleteById(id);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':roleId/users')
|
@Post(':roleId/users')
|
||||||
async addUserToRole(
|
async addUserToRole(
|
||||||
@TenantId() tenantId: string,
|
@TenantId() tenantId: string,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Param,
|
Param,
|
||||||
Body,
|
Body,
|
||||||
@@ -57,6 +58,47 @@ export class SetupUsersController {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
async updateUser(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() data: { email?: string; password?: string; firstName?: string; lastName?: string },
|
||||||
|
) {
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
const updateData: any = {};
|
||||||
|
|
||||||
|
if (data.email) updateData.email = data.email;
|
||||||
|
if (data.firstName !== undefined) updateData.firstName = data.firstName;
|
||||||
|
if (data.lastName !== undefined) updateData.lastName = data.lastName;
|
||||||
|
|
||||||
|
// Hash password if provided
|
||||||
|
if (data.password) {
|
||||||
|
updateData.password = await bcrypt.hash(data.password, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.query(knex).patchAndFetchById(id, updateData);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteUser(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
) {
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
// Delete user role assignments first
|
||||||
|
await knex('user_roles').where({ userId: id }).delete();
|
||||||
|
|
||||||
|
// Delete the user
|
||||||
|
await User.query(knex).deleteById(id);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':userId/roles')
|
@Post(':userId/roles')
|
||||||
async addRoleToUser(
|
async addRoleToUser(
|
||||||
@TenantId() tenantId: string,
|
@TenantId() tenantId: string,
|
||||||
|
|||||||
@@ -49,9 +49,17 @@
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{{ formatDate(role.createdAt) }}</TableCell>
|
<TableCell>{{ formatDate(role.createdAt) }}</TableCell>
|
||||||
<TableCell class="text-right" @click.stop>
|
<TableCell class="text-right" @click.stop>
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
<Button variant="ghost" size="icon" @click="navigateTo(`/setup/roles/${role.id}`)">
|
<Button variant="ghost" size="icon" @click="navigateTo(`/setup/roles/${role.id}`)">
|
||||||
<Eye class="h-4 w-4" />
|
<Eye class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" @click="openEditDialog(role)">
|
||||||
|
<Edit class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" @click="openDeleteDialog(role)">
|
||||||
|
<Trash2 class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -97,6 +105,64 @@
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Edit Role Dialog -->
|
||||||
|
<Dialog v-model:open="showEditDialog">
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Role</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update role information
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-name">Name</Label>
|
||||||
|
<Input id="edit-name" v-model="editRole.name" placeholder="Role name" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-description">Description</Label>
|
||||||
|
<Input id="edit-description" v-model="editRole.description" placeholder="Role description" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-guardName">Guard Name</Label>
|
||||||
|
<Select v-model="editRole.guardName" @update:model-value="(value) => editRole.guardName = value">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select guard" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="tenant">Tenant</SelectItem>
|
||||||
|
<SelectItem value="central">Central</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="showEditDialog = false">Cancel</Button>
|
||||||
|
<Button @click="updateRole" :disabled="!editRole.name">
|
||||||
|
Update Role
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<Dialog v-model:open="showDeleteDialog">
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Role</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this role? This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="showDeleteDialog = false">Cancel</Button>
|
||||||
|
<Button variant="destructive" @click="deleteRole">
|
||||||
|
Delete Role
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</main>
|
</main>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +177,7 @@ import { Input } from '~/components/ui/input';
|
|||||||
import { Label } from '~/components/ui/label';
|
import { Label } from '~/components/ui/label';
|
||||||
import { Badge } from '~/components/ui/badge';
|
import { Badge } from '~/components/ui/badge';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
|
||||||
import { Plus, Eye } from 'lucide-vue-next';
|
import { Plus, Eye, Edit, Trash2 } from 'lucide-vue-next';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'default',
|
layout: 'default',
|
||||||
@@ -123,11 +189,20 @@ const { toast } = useToast();
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const roles = ref<any[]>([]);
|
const roles = ref<any[]>([]);
|
||||||
const showCreateDialog = ref(false);
|
const showCreateDialog = ref(false);
|
||||||
|
const showEditDialog = ref(false);
|
||||||
|
const showDeleteDialog = ref(false);
|
||||||
const newRole = ref({
|
const newRole = ref({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
guardName: 'tenant',
|
guardName: 'tenant',
|
||||||
});
|
});
|
||||||
|
const editRole = ref({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
guardName: 'tenant',
|
||||||
|
});
|
||||||
|
const roleToDelete = ref<any>(null);
|
||||||
|
|
||||||
const loadRoles = async () => {
|
const loadRoles = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -155,6 +230,50 @@ const createRole = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditDialog = (role: any) => {
|
||||||
|
editRole.value = {
|
||||||
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
|
description: role.description || '',
|
||||||
|
guardName: role.guardName || 'tenant',
|
||||||
|
};
|
||||||
|
showEditDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRole = async () => {
|
||||||
|
try {
|
||||||
|
await api.patch(`/setup/roles/${editRole.value.id}`, {
|
||||||
|
name: editRole.value.name,
|
||||||
|
description: editRole.value.description,
|
||||||
|
guardName: editRole.value.guardName,
|
||||||
|
});
|
||||||
|
toast.success('Role updated successfully');
|
||||||
|
showEditDialog.value = false;
|
||||||
|
await loadRoles();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to update role:', error);
|
||||||
|
toast.error(error.message || 'Failed to update role');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteDialog = (role: any) => {
|
||||||
|
roleToDelete.value = role;
|
||||||
|
showDeleteDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRole = async () => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/setup/roles/${roleToDelete.value.id}`);
|
||||||
|
toast.success('Role deleted successfully');
|
||||||
|
showDeleteDialog.value = false;
|
||||||
|
roleToDelete.value = null;
|
||||||
|
await loadRoles();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to delete role:', error);
|
||||||
|
toast.error(error.message || 'Failed to delete role');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatDate = (date: string) => {
|
const formatDate = (date: string) => {
|
||||||
if (!date) return 'N/A';
|
if (!date) return 'N/A';
|
||||||
return new Date(date).toLocaleDateString();
|
return new Date(date).toLocaleDateString();
|
||||||
|
|||||||
@@ -52,9 +52,17 @@
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{{ formatDate(user.createdAt) }}</TableCell>
|
<TableCell>{{ formatDate(user.createdAt) }}</TableCell>
|
||||||
<TableCell class="text-right" @click.stop>
|
<TableCell class="text-right" @click.stop>
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
<Button variant="ghost" size="icon" @click="navigateTo(`/setup/users/${user.id}`)">
|
<Button variant="ghost" size="icon" @click="navigateTo(`/setup/users/${user.id}`)">
|
||||||
<Eye class="h-4 w-4" />
|
<Eye class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" @click="openEditDialog(user)">
|
||||||
|
<Edit class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" @click="openDeleteDialog(user)">
|
||||||
|
<Trash2 class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -96,6 +104,60 @@
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Edit User Dialog -->
|
||||||
|
<Dialog v-model:open="showEditDialog">
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit User</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update user information
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-email">Email</Label>
|
||||||
|
<Input id="edit-email" v-model="editUser.email" type="email" placeholder="user@example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-password">Password (leave blank to keep current)</Label>
|
||||||
|
<Input id="edit-password" v-model="editUser.password" type="password" placeholder="••••••••" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-firstName">First Name</Label>
|
||||||
|
<Input id="edit-firstName" v-model="editUser.firstName" placeholder="John" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-lastName">Last Name</Label>
|
||||||
|
<Input id="edit-lastName" v-model="editUser.lastName" placeholder="Doe" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="showEditDialog = false">Cancel</Button>
|
||||||
|
<Button @click="updateUser" :disabled="!editUser.email">
|
||||||
|
Update User
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<Dialog v-model:open="showDeleteDialog">
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete User</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this user? This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="showDeleteDialog = false">Cancel</Button>
|
||||||
|
<Button variant="destructive" @click="deleteUser">
|
||||||
|
Delete User
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</main>
|
</main>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +171,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
|||||||
import { Input } from '~/components/ui/input';
|
import { Input } from '~/components/ui/input';
|
||||||
import { Label } from '~/components/ui/label';
|
import { Label } from '~/components/ui/label';
|
||||||
import { Badge } from '~/components/ui/badge';
|
import { Badge } from '~/components/ui/badge';
|
||||||
import { UserPlus, Eye } from 'lucide-vue-next';
|
import { UserPlus, Eye, Edit, Trash2 } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
@@ -118,12 +180,22 @@ const { toast } = useToast();
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const users = ref<any[]>([]);
|
const users = ref<any[]>([]);
|
||||||
const showCreateDialog = ref(false);
|
const showCreateDialog = ref(false);
|
||||||
|
const showEditDialog = ref(false);
|
||||||
|
const showDeleteDialog = ref(false);
|
||||||
const newUser = ref({
|
const newUser = ref({
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
});
|
});
|
||||||
|
const editUser = ref({
|
||||||
|
id: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
});
|
||||||
|
const userToDelete = ref<any>(null);
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -151,6 +223,55 @@ const createUser = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditDialog = (user: any) => {
|
||||||
|
editUser.value = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
password: '',
|
||||||
|
firstName: user.firstName || '',
|
||||||
|
lastName: user.lastName || '',
|
||||||
|
};
|
||||||
|
showEditDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = async () => {
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
email: editUser.value.email,
|
||||||
|
firstName: editUser.value.firstName,
|
||||||
|
lastName: editUser.value.lastName,
|
||||||
|
};
|
||||||
|
if (editUser.value.password) {
|
||||||
|
payload.password = editUser.value.password;
|
||||||
|
}
|
||||||
|
await api.patch(`/setup/users/${editUser.value.id}`, payload);
|
||||||
|
toast.success('User updated successfully');
|
||||||
|
showEditDialog.value = false;
|
||||||
|
await loadUsers();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to update user:', error);
|
||||||
|
toast.error(error.message || 'Failed to update user');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteDialog = (user: any) => {
|
||||||
|
userToDelete.value = user;
|
||||||
|
showDeleteDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async () => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/setup/users/${userToDelete.value.id}`);
|
||||||
|
toast.success('User deleted successfully');
|
||||||
|
showDeleteDialog.value = false;
|
||||||
|
userToDelete.value = null;
|
||||||
|
await loadUsers();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to delete user:', error);
|
||||||
|
toast.error(error.message || 'Failed to delete user');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getUserName = (user: any) => {
|
const getUserName = (user: any) => {
|
||||||
if (user.firstName || user.lastName) {
|
if (user.firstName || user.lastName) {
|
||||||
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
||||||
|
|||||||
Reference in New Issue
Block a user