permissions refactoring
This commit is contained in:
@@ -23,9 +23,9 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { dashboard } from '@/routes';
|
||||
import { index as adminTeams } from '@/routes/admin/teams';
|
||||
import { index as adminUsers } from '@/routes/admin/users';
|
||||
import { index as projectsIndex } from '@/routes/projects';
|
||||
import { index as teamsIndex } from '@/routes/teams';
|
||||
import type { NavItem } from '@/types';
|
||||
|
||||
const page = usePage();
|
||||
@@ -44,16 +44,18 @@ const mainNavItems = computed<NavItem[]>(() => {
|
||||
},
|
||||
];
|
||||
|
||||
if (page.props.auth.user.is_admin) {
|
||||
if (page.props.auth.permissions.globalAdmin) {
|
||||
items.push({
|
||||
title: 'Users',
|
||||
href: adminUsers(),
|
||||
icon: Shield,
|
||||
});
|
||||
}
|
||||
|
||||
if (page.props.auth.permissions.canManageTeams) {
|
||||
items.push({
|
||||
title: 'Teams',
|
||||
href: adminTeams(),
|
||||
href: teamsIndex(),
|
||||
icon: Users,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import { Shield, Users } from 'lucide-vue-next';
|
||||
import { FolderPlus, Shield, Users } from 'lucide-vue-next';
|
||||
import UserManagementController from '@/actions/App/Http/Controllers/Admin/UserManagementController';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -16,7 +16,7 @@ import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { index as adminUsers } from '@/routes/admin/users';
|
||||
import type { AdminUsersPageProps, BreadcrumbItem } from '@/types';
|
||||
|
||||
defineProps<AdminUsersPageProps>();
|
||||
const props = defineProps<AdminUsersPageProps>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
@@ -24,6 +24,9 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
href: adminUsers(),
|
||||
},
|
||||
];
|
||||
|
||||
const hasPermission = (actorId: number, permission: string) =>
|
||||
(props.directPermissions[actorId] ?? []).includes(permission);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,7 +37,7 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<Heading
|
||||
title="User management"
|
||||
description="Manage application-wide admin access for existing users."
|
||||
description="Assign the global permissions that unlock platform-wide administration and project creation."
|
||||
/>
|
||||
|
||||
<Badge variant="outline" class="gap-1.5 px-3 py-1">
|
||||
@@ -54,8 +57,8 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
<CardHeader class="border-b">
|
||||
<CardTitle>Users</CardTitle>
|
||||
<CardDescription>
|
||||
Promote trusted users to platform admins or remove admin
|
||||
access when it is no longer needed.
|
||||
Global permissions are assigned directly to users and
|
||||
apply across the entire application.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -68,16 +71,16 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table
|
||||
class="divide-border min-w-full divide-y text-sm"
|
||||
>
|
||||
<table class="divide-border min-w-full divide-y text-sm">
|
||||
<thead
|
||||
class="bg-muted/40 text-muted-foreground text-left"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-medium">User</th>
|
||||
<th class="px-4 py-3 font-medium">Email</th>
|
||||
<th class="px-4 py-3 font-medium">Role</th>
|
||||
<th class="px-4 py-3 font-medium">
|
||||
Current access
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-right font-medium"
|
||||
>
|
||||
@@ -89,12 +92,10 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
<tr
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="bg-background"
|
||||
class="bg-background align-top"
|
||||
>
|
||||
<td class="px-4 py-4">
|
||||
<div
|
||||
class="text-foreground font-medium"
|
||||
>
|
||||
<div class="text-foreground font-medium">
|
||||
{{ user.name }}
|
||||
</div>
|
||||
</td>
|
||||
@@ -102,19 +103,43 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
{{ user.email }}
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<Badge
|
||||
:variant="
|
||||
user.is_admin
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
"
|
||||
class="gap-1.5"
|
||||
>
|
||||
<Shield class="size-3.5" />
|
||||
{{
|
||||
user.is_admin ? 'Admin' : 'User'
|
||||
}}
|
||||
</Badge>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge
|
||||
v-if="
|
||||
hasPermission(
|
||||
user.id,
|
||||
'global:admin',
|
||||
)
|
||||
"
|
||||
class="gap-1.5"
|
||||
>
|
||||
<Shield class="size-3.5" />
|
||||
Global admin
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="
|
||||
hasPermission(
|
||||
user.id,
|
||||
'project:create',
|
||||
)
|
||||
"
|
||||
variant="secondary"
|
||||
class="gap-1.5"
|
||||
>
|
||||
<FolderPlus class="size-3.5" />
|
||||
Project create
|
||||
</Badge>
|
||||
<span
|
||||
v-if="
|
||||
(directPermissions[
|
||||
user.id
|
||||
] ?? []).length === 0
|
||||
"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
No direct global permissions
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex justify-end">
|
||||
@@ -124,33 +149,40 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
user.id,
|
||||
)
|
||||
"
|
||||
class="flex flex-wrap justify-end gap-3"
|
||||
v-slot="{ processing }"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="is_admin"
|
||||
:value="
|
||||
user.is_admin
|
||||
? '0'
|
||||
: '1'
|
||||
<label
|
||||
v-for="option in permissionOptions"
|
||||
:key="
|
||||
`${user.id}-${option.value}`
|
||||
"
|
||||
/>
|
||||
class="border-border inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions[]"
|
||||
:value="option.value"
|
||||
:checked="
|
||||
hasPermission(
|
||||
user.id,
|
||||
option.value,
|
||||
)
|
||||
"
|
||||
class="border-input text-primary focus:ring-ring size-4 rounded"
|
||||
/>
|
||||
<span>{{ option.label }}</span>
|
||||
</label>
|
||||
|
||||
<div class="w-full sm:hidden" />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
:variant="
|
||||
user.is_admin
|
||||
? 'outline'
|
||||
: 'default'
|
||||
"
|
||||
variant="outline"
|
||||
:disabled="processing"
|
||||
>
|
||||
{{
|
||||
user.is_admin
|
||||
? 'Remove admin'
|
||||
: 'Make admin'
|
||||
}}
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ const hasPermission = (
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
v-if="$page.props.auth.user.is_admin"
|
||||
v-if="$page.props.auth.permissions.globalAdmin"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
description="Create and browse project spaces that will later host boards, backlog, and settings."
|
||||
/>
|
||||
|
||||
<Button as-child>
|
||||
<Button v-if="$page.props.auth.permissions.projectCreate" as-child>
|
||||
<Link :href="create()">
|
||||
<Plus class="size-4" />
|
||||
New project
|
||||
|
||||
@@ -107,7 +107,7 @@ const projectAreas = [
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="$page.props.auth.user.is_admin"
|
||||
v-if="$page.props.auth.permissions.globalAdmin"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
|
||||
@@ -61,7 +61,7 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
v-if="$page.props.auth.user.is_admin"
|
||||
v-if="$page.props.auth.permissions.globalAdmin"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
|
||||
@@ -65,7 +65,7 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
v-if="$page.props.auth.user.is_admin"
|
||||
v-if="$page.props.auth.permissions.globalAdmin"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
|
||||
@@ -234,7 +234,7 @@ const submitUpdate = (): void => {
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
v-if="$page.props.auth.user.is_admin"
|
||||
v-if="$page.props.auth.permissions.globalAdmin"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { index as teamsIndex } from '@/routes/admin/teams';
|
||||
import { index as teamsIndex } from '@/routes/teams';
|
||||
import type { BreadcrumbItem, TeamsIndexPageProps } from '@/types';
|
||||
|
||||
defineProps<TeamsIndexPageProps>();
|
||||
const props = defineProps<TeamsIndexPageProps>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
@@ -27,6 +27,15 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
href: teamsIndex(),
|
||||
},
|
||||
];
|
||||
|
||||
const hasPermission = (
|
||||
permissions: Record<string, string[]>,
|
||||
actorId: number,
|
||||
permission: string,
|
||||
) => (permissions[actorId] ?? []).includes(permission);
|
||||
|
||||
const permissionsForTeam = (teamId: number): Record<string, string[]> =>
|
||||
props.teamPermissions[teamId] ?? {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,12 +48,16 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
>
|
||||
<Heading
|
||||
title="Teams"
|
||||
description="Create reusable teams and manage member assignments for inherited project permissions."
|
||||
description="Create reusable teams, manage memberships, and assign team-scoped permissions to users."
|
||||
/>
|
||||
|
||||
<Badge variant="outline" class="gap-1.5 px-3 py-1">
|
||||
<Shield class="size-3.5" />
|
||||
Admin-only area
|
||||
{{
|
||||
canCreateTeams
|
||||
? 'Global admin'
|
||||
: 'Team-scoped access'
|
||||
}}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -55,8 +68,15 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[22rem_minmax(0,1fr)]">
|
||||
<Card>
|
||||
<div
|
||||
class="grid gap-6"
|
||||
:class="
|
||||
canCreateTeams
|
||||
? 'xl:grid-cols-[22rem_minmax(0,1fr)]'
|
||||
: 'xl:grid-cols-1'
|
||||
"
|
||||
>
|
||||
<Card v-if="canCreateTeams">
|
||||
<CardHeader>
|
||||
<CardTitle>Create team</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -121,7 +141,7 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="pt-6">
|
||||
<CardContent class="space-y-6 pt-6">
|
||||
<Form
|
||||
v-bind="
|
||||
TeamController.updateMembers.form(team.id)
|
||||
@@ -164,12 +184,177 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
Save members
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<div class="space-y-4 border-t pt-6">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-foreground font-medium">
|
||||
Team permissions
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Direct user permissions on this team are
|
||||
stored separately from team membership.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table
|
||||
class="divide-border min-w-full divide-y text-sm"
|
||||
>
|
||||
<thead
|
||||
class="bg-muted/40 text-muted-foreground text-left"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
class="px-4 py-3 font-medium"
|
||||
>
|
||||
User
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 font-medium"
|
||||
>
|
||||
Current permissions
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-right font-medium"
|
||||
>
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-border divide-y">
|
||||
<tr
|
||||
v-for="user in users"
|
||||
:key="`${team.id}-${user.id}`"
|
||||
class="bg-background align-top"
|
||||
>
|
||||
<td class="px-4 py-4">
|
||||
<div
|
||||
class="text-foreground font-medium"
|
||||
>
|
||||
{{ user.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<Badge
|
||||
v-for="option in permissionOptions"
|
||||
v-show="
|
||||
hasPermission(
|
||||
permissionsForTeam(
|
||||
team.id,
|
||||
),
|
||||
user.id,
|
||||
option.value,
|
||||
)
|
||||
"
|
||||
:key="
|
||||
`${team.id}-${user.id}-${option.value}-badge`
|
||||
"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ option.label }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="
|
||||
(
|
||||
permissionsForTeam(
|
||||
team.id,
|
||||
)[user.id] ??
|
||||
[]
|
||||
).length === 0
|
||||
"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
No direct team
|
||||
permissions
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div
|
||||
class="flex justify-end"
|
||||
>
|
||||
<Form
|
||||
v-bind="
|
||||
TeamController.updateUserPermissions.form(
|
||||
[
|
||||
team.id,
|
||||
user.id,
|
||||
],
|
||||
)
|
||||
"
|
||||
class="flex flex-wrap justify-end gap-3"
|
||||
v-slot="{
|
||||
processing,
|
||||
}"
|
||||
>
|
||||
<label
|
||||
v-for="option in permissionOptions"
|
||||
:key="
|
||||
`${team.id}-${user.id}-${option.value}`
|
||||
"
|
||||
class="border-border inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions[]"
|
||||
:value="
|
||||
option.value
|
||||
"
|
||||
:checked="
|
||||
hasPermission(
|
||||
permissionsForTeam(
|
||||
team.id,
|
||||
),
|
||||
user.id,
|
||||
option.value,
|
||||
)
|
||||
"
|
||||
class="border-input text-primary focus:ring-ring size-4 rounded"
|
||||
/>
|
||||
<span>
|
||||
{{
|
||||
option.label
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="w-full sm:hidden"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
:disabled="
|
||||
processing
|
||||
"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="teams.length === 0">
|
||||
<CardContent class="text-muted-foreground p-6 text-sm">
|
||||
No teams exist yet.
|
||||
No teams are available for your current
|
||||
permissions.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,14 @@ import type { UserModel } from '@/types/models/user-model';
|
||||
|
||||
export type User = UserModel;
|
||||
|
||||
export type AuthPermissions = {
|
||||
canManageTeams: boolean;
|
||||
globalAdmin: boolean;
|
||||
projectCreate: boolean;
|
||||
};
|
||||
|
||||
export type Auth = {
|
||||
permissions: AuthPermissions;
|
||||
user: User;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ export type UserModel = {
|
||||
email: string;
|
||||
avatar?: string;
|
||||
email_verified_at: string | null;
|
||||
is_admin: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ import type { UserGitConnectionModel } from '@/types/models/user-git-connection-
|
||||
import type { UserModel } from '@/types/models/user-model';
|
||||
|
||||
export type AdminUsersPageProps = {
|
||||
directPermissions: Record<string, string[]>;
|
||||
permissionOptions: PermissionOption[];
|
||||
users: UserModel[];
|
||||
status?: string | null;
|
||||
};
|
||||
@@ -31,14 +33,14 @@ export type ProjectShowPageProps = {
|
||||
project: ProjectModel;
|
||||
};
|
||||
|
||||
export type ProjectPermissionOption = {
|
||||
export type PermissionOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type ProjectAccessPageProps = {
|
||||
directPermissions: Record<string, string[]>;
|
||||
permissionOptions: ProjectPermissionOption[];
|
||||
permissionOptions: PermissionOption[];
|
||||
project: ProjectModel;
|
||||
status?: string | null;
|
||||
teamPermissions: Record<string, string[]>;
|
||||
@@ -47,7 +49,10 @@ export type ProjectAccessPageProps = {
|
||||
};
|
||||
|
||||
export type TeamsIndexPageProps = {
|
||||
canCreateTeams: boolean;
|
||||
permissionOptions: PermissionOption[];
|
||||
status?: string | null;
|
||||
teamPermissions: Record<string, Record<string, string[]>>;
|
||||
teams: TeamModel[];
|
||||
users: UserModel[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user