diff --git a/app/Console/Commands/LdapSyncRoles.php b/app/Console/Commands/LdapSyncRoles.php index c7e984e..1898c5f 100644 --- a/app/Console/Commands/LdapSyncRoles.php +++ b/app/Console/Commands/LdapSyncRoles.php @@ -4,11 +4,14 @@ use App\Ldap\Committee; use App\Ldap\Community; +use App\Ldap\Group; use App\Ldap\Role; +use App\Models\GroupMembership; use App\Models\RoleMembership; use Carbon\Carbon; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Isolatable; +use Illuminate\Support\Facades\DB; class LdapSyncRoles extends Command { @@ -45,6 +48,9 @@ public function handle() ->setDn(Community::$rootDn) ->search('ou', $this->argument('community')) ->get(); + + $this->comment("Committees:"); + foreach ($realms as $realm){ $committees = Committee::fromCommunity($realm->getFirstAttribute('ou')) ->search('ou', $this->argument('committee')) @@ -60,7 +66,7 @@ public function handle() ->where('committee_dn', $committee->getDn()) ->where('role_cn', $role->getFirstAttribute('cn')) ->get(); - $this->comment(" |-> " .$role->getDn()); + $this->comment(" |-> " . $role->getDn()); // delete all members so far $role->setAttribute('uniqueMember', ['']); $ldapMembers = $role->members(); @@ -73,5 +79,38 @@ public function handle() } } } + + $this->comment("\nGroups:"); + + foreach ($realms as $realm) { + $groups = Group::fromCommunity($realm->getFirstAttribute('ou')) + ->search('ou', $this->argument('group')) + ->get(); + + foreach ($groups as $group) { + $this->comment("> " . $group->getDn()); + + // delete all members so far + $group->setAttribute('uniqueMember', ['']); + + $roles = GroupMembership::where('group_dn', $group->getDn())->get(); + + foreach ($roles as $role) { + $roleCn = str_replace('cn=', '', substr($role->role_dn, 0, strpos($role->role_dn, ','))); + $committeeDn = strstr($role->role_dn, "ou="); + $activeMemberships = RoleMembership::active($date) + ->where('committee_dn', $committeeDn) + ->where('role_cn', $roleCn) + ->get(); + + $ldapMembers = $group->users(); + foreach ($activeMemberships as $membership) { + // add only active members back + $this->comment(" |-> $membership->username"); + $ldapMembers->attach($membership->user->ldap()); + } + } + } + } } } diff --git a/app/Console/Commands/MoveGroupRolesFromLdapToDatabase.php b/app/Console/Commands/MoveGroupRolesFromLdapToDatabase.php new file mode 100644 index 0000000..3f1d31b --- /dev/null +++ b/app/Console/Commands/MoveGroupRolesFromLdapToDatabase.php @@ -0,0 +1,60 @@ +list() // only first level + ->setDn(Community::$rootDn) + ->search('ou', $this->argument('community')) + ->get(); + + foreach ($realms as $realm) { + $groups = Group::fromCommunity($realm->getFirstAttribute('ou')) + ->search('ou', $this->argument('group')) + ->get(); + + foreach ($groups as $group) { + $this->comment("> " . $group->getDn()); + + // get roles + $roles = $group->members()->get(); + + foreach ($roles as $role) { + $this->comment($role); + GroupMembership::create([ + 'group_dn' => $group->getDn(), + 'role_dn' => $role, + ]); + } + } + } + } +} diff --git a/app/Livewire/ChangePassword.php b/app/Livewire/ChangePassword.php index 684a657..73744f8 100644 --- a/app/Livewire/ChangePassword.php +++ b/app/Livewire/ChangePassword.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules\Password; +use Livewire\Attributes\Locked; use Livewire\Attributes\Rule; use Livewire\Component; use Mockery\Generator\StringManipulation\Pass\Pass; @@ -18,6 +19,20 @@ class ChangePassword extends Component public string $password_confirmation; + #[Locked] + public $currentUsername; + + public function mount($username) + { + if ($username === auth()->user()->username || auth()->user()->can('superadmin', User::class)) { + $this->currentUsername = $username; + } elseif ($username === auth()->user()->username) { + $this->currentUsername = auth()->user()->username; + } else { + abort('403'); + } + } + public function rules(): array { return [ diff --git a/app/Livewire/Group/AddRoleToGroup.php b/app/Livewire/Group/AddRoleToGroup.php index 78e2045..6974567 100644 --- a/app/Livewire/Group/AddRoleToGroup.php +++ b/app/Livewire/Group/AddRoleToGroup.php @@ -9,6 +9,8 @@ use Livewire\Attributes\Locked; use Livewire\Component; +use App\Models\GroupMembership; + class AddRoleToGroup extends Component { #[Locked] @@ -42,9 +44,12 @@ public function render() public function save() { - /** @var Group $group */ - $group = Group::findOrFail(Group::dnFrom($this->uid, $this->group_cn)); - $group->members()->attach($this->selected_role_dn); + $group_dn = Group::findOrFail(Group::dnFrom($this->uid, $this->group_cn)); + GroupMembership::create([ + 'group_dn' => $group_dn, + 'role_dn' => $this->selected_role_dn, + ]); + return redirect()->route('realms.groups.roles', ['uid' => $this->uid, 'cn' => $this->group_cn]) ->with('message', __('groups.success_role_add')) ; diff --git a/app/Livewire/Group/ListRolesInGroup.php b/app/Livewire/Group/ListRolesInGroup.php index d21b0dc..a38c127 100644 --- a/app/Livewire/Group/ListRolesInGroup.php +++ b/app/Livewire/Group/ListRolesInGroup.php @@ -5,6 +5,7 @@ use App\Ldap\Community; use App\Ldap\Group; use App\Ldap\Role; +use App\Models\GroupMembership; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; @@ -51,16 +52,15 @@ public function updatedSearch(): void } public function render() { - /** @var Group $group */ - $group = Group::findOrFail($this->group_dn); - $roles = $group->members()->get(); - $users = $group->users()->get(); - // slice breaks it, whyever - get to go. + $rolesDB = GroupMembership::select('role_dn')->where('group_dn', $this->group_dn)->get(); + $roles = []; + foreach ($rolesDB as $row) { + $role = Role::findOrFail($row->role_dn); + array_push($roles, $role); + } return view( 'livewire.group.roles', [ 'roles' => $roles, - 'users' => $users, - 'group' => $group, ] )->title(__('groups.roles_list_title', ['name' => $this->group_cn])); } @@ -75,14 +75,7 @@ public function deletePrepare(string $role_dn): void $committee = $role->committee(); $this->deleteRoleDN = $role_dn; - $this->deleteRoleName = [ - 'role_short' => $role->getFirstAttribute('description'), - 'role_name' => $role->getFirstAttribute('cn'), - 'committee_name' => $committee?->getFirstAttribute('description'), - 'committee_short' => $committee?->getFirstAttribute('ou'), - 'group_short' => $group->getFirstAttribute('cn'), - 'group_name' => $group->getFirstAttribute('description') - ]; + $this->deleteRoleName = [ $role->getFirstAttribute('cn') ]; $this->showDeleteModal = true; } @@ -92,10 +85,7 @@ public function deleteCommit(): void $community = Community::findByUid($this->realm_uid); $this->authorize('delete', [Group::class, $community]); - $group = Group::findOrFail($this->group_dn); - $role = Role::findOrFail($this->deleteRoleDN); - - $group->roles()->detach($role); + GroupMembership::where('group_dn', $this->group_dn)->where('role_dn', $this->deleteRoleDN)->delete(); $this->close(); } diff --git a/app/Livewire/Profile.php b/app/Livewire/Profile.php index 5132d54..3de2109 100644 --- a/app/Livewire/Profile.php +++ b/app/Livewire/Profile.php @@ -33,10 +33,18 @@ class Profile extends Component public $picture; public $pictureUrl; - public function mount() + public $currentUsername; + + public function mount($username) { - $username = Auth::user()->username; - $user = User::findOrFailByUsername($username); + if ($username == auth()->user()->username || auth()->user()->can('superadmin', User::class)) { + $this->currentUsername = $username; + } elseif ($username == auth()->user()->username) { + $this->currentUsername = auth()->user()->username; + } else { + abort('403'); + } + $user = User::findOrFailByUsername($this->currentUsername); $this->uid = $user->getFirstAttribute('uid'); $this->givenName = $user->getFirstAttribute('givenName'); $this->sn = $user->getFirstAttribute('sn'); @@ -57,9 +65,6 @@ public function render() public function save() { $this->validate(); - if (Auth::user()->username !== $this->uid) { - abort('403'); - } $user = User::findOrFailByUsername($this->uid); $user->setAttribute('mail', $this->email); $user->setAttribute('givenName', $this->givenName); diff --git a/app/Livewire/Profile/Memberships.php b/app/Livewire/Profile/Memberships.php new file mode 100644 index 0000000..4cdddbd --- /dev/null +++ b/app/Livewire/Profile/Memberships.php @@ -0,0 +1,74 @@ +user()->username || auth()->user()->can('superadmin', User::class)) { + $this->currentUsername = $username; + } elseif ($username == auth()->user()->username) { + $this->currentUsername = auth()->user()->username; + } else { + abort('403'); + } + } + + public function getMemberships(string $username, bool $onlyActive) + { + $query = RoleMembership::where('username', $username); + if ($onlyActive) { + $query->whereNull('until'); + } + $roleMemberships = $query->get(); + $memberships = []; + foreach ($roleMemberships as $row) { + $role = Role::findOrFail('cn=' . $row->role_cn . ',' . $row->committee_dn); + $memberships[] = [ + 'role' => $role, + 'from' => $row->from, + 'until' => $row->until, + 'decided' => $row->decided, + 'comment' => $row->comment, + ]; + } + return $memberships; + } + + public function render() + { + $memberships = $this->getMemberships($this->currentUsername, $this->showOnlyActive); + + return view('livewire.profile.memberships', [ + 'memberships' => $memberships, + ])->title(__('Profile')); + } + + public function exportPdf() + { + $memberships = $this->getMemberships($this->currentUsername, false); + $user = User::findOrFailByUsername($this->currentUsername); + $pdf = Pdf::loadView('pdfs.memberships', [ + 'fullName' => $user->cn[0], + 'community' => null, + 'memberships' => $memberships, + ]); + + return response()->streamDownload(function () use ($pdf) { + echo $pdf->stream(); + }, strtolower(trans('profile.memberships')) . '_' . $this->currentUsername . '.pdf'); + } +} diff --git a/app/Livewire/Realm/ListMembers.php b/app/Livewire/Realm/ListMembers.php index 754a9d8..9d1dd6d 100644 --- a/app/Livewire/Realm/ListMembers.php +++ b/app/Livewire/Realm/ListMembers.php @@ -4,6 +4,7 @@ use App\Ldap\Community; use App\Ldap\User; +use Barryvdh\DomPDF\Facade\Pdf; use Livewire\Attributes\Rule; use Livewire\Attributes\Url; use Livewire\Component; @@ -87,4 +88,20 @@ public function close(): void $this->showDeleteModal = false; unset($this->deleteMemberName, $this->deleteMemberUsername); } + + public function exportPdf($username) + { + $memberships = app('App\Livewire\Profile\Memberships')->getMemberships($username, false); + $user = User::findOrFailByUsername($username); + $community = Community::findOrFailByUid($this->community_name); + $pdf = Pdf::loadView('pdfs.memberships', [ + 'fullName' => $user->cn[0], + 'community' => $community->description[0], + 'memberships' => $memberships, + ]); + + return response()->streamDownload(function () use ($pdf) { + echo $pdf->stream(); + }, 'memberships-' . $username . '.pdf');; + } } diff --git a/app/Models/GroupMembership.php b/app/Models/GroupMembership.php new file mode 100644 index 0000000..1e0cb05 --- /dev/null +++ b/app/Models/GroupMembership.php @@ -0,0 +1,37 @@ +hasMany(Role::class); + } +} diff --git a/composer.json b/composer.json index 35b0c1e..8c27bd7 100644 --- a/composer.json +++ b/composer.json @@ -10,10 +10,11 @@ "ext-gd": "*", "ext-ldap": "*", "ext-pdo": "*", + "barryvdh/laravel-dompdf": "^3.1", "dacoto/laravel-domain-validation": "^3.0", "diglactic/laravel-breadcrumbs": "^8.1", - "directorytree/ldaprecord-laravel": "^3.0", "directorytree/ldaprecord": "v3.5.1", + "directorytree/ldaprecord-laravel": "^3.0", "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^10.0", "laravel/passport": "^11.10", diff --git a/database/migrations/2025_06_27_095320_create_role_group_relation.php b/database/migrations/2025_06_27_095320_create_role_group_relation.php new file mode 100644 index 0000000..781b42d --- /dev/null +++ b/database/migrations/2025_06_27_095320_create_role_group_relation.php @@ -0,0 +1,29 @@ +id(); + $table->string('group_dn'); + $table->string('role_dn'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('role_group_relation'); + } +}; diff --git a/lang/de/profile.php b/lang/de/profile.php new file mode 100644 index 0000000..36af463 --- /dev/null +++ b/lang/de/profile.php @@ -0,0 +1,18 @@ + 'Tätigkeit', + 'activities' => 'Tätigkeiten', + 'comment' => 'Bemerkung', + 'committee' => 'Gremium', + 'decision' => 'Beschluss', + 'exportAsPdf' => 'Als PDF exportieren', + 'from' => 'von', + 'in' => 'in', + 'memberships' => 'Mitgliedschaften', + 'membershipsAsPdf' => 'Mitgliedschaften als PDF', + 'role' => 'Rolle', + 'showOnlyActiveMemberships' => 'Zeige nur aktive Mitgliedschaften', + 'today' => 'heute', + 'until' => 'bis', +]; \ No newline at end of file diff --git a/lang/en/profile.php b/lang/en/profile.php new file mode 100644 index 0000000..914c8f9 --- /dev/null +++ b/lang/en/profile.php @@ -0,0 +1,18 @@ + 'Activity', + 'activities' => 'Activities', + 'comment' => 'Comment', + 'committee' => 'Committee', + 'decision' => 'Decision', + 'exportAsPdf' => 'Export as PDF', + 'from' => 'from', + 'in' => 'in', + 'memberships' => 'Memberships', + 'membershipsAsPdf' => 'Memberships as PDF', + 'role' => 'Role', + 'showOnlyActiveMemberships' => 'Show only active memberships', + 'today' => 'today', + 'until' => 'until', +]; \ No newline at end of file diff --git a/resources/views/components/navigation.blade.php b/resources/views/components/navigation.blade.php index c16c72f..b8fe48a 100644 --- a/resources/views/components/navigation.blade.php +++ b/resources/views/components/navigation.blade.php @@ -64,10 +64,7 @@ - {{ __('Profile') }} - {{ __('Change Password') }} - -
+ {{ __('Profile') }}
@@ -141,7 +138,7 @@
{{ Auth::user()->email }}
+ :href="route('profile', ['username' => auth()->user()->username])"> {{ __('Profil') }}
diff --git a/resources/views/livewire/change-password.blade.php b/resources/views/livewire/change-password.blade.php index 25fd447..312c6ee 100644 --- a/resources/views/livewire/change-password.blade.php +++ b/resources/views/livewire/change-password.blade.php @@ -1,9 +1,12 @@ -
-
-

{{ __('Change Password') }}

+ + {{ __('user.help.password') }} diff --git a/resources/views/livewire/profile.blade.php b/resources/views/livewire/profile.blade.php index ea9fc72..d803e13 100644 --- a/resources/views/livewire/profile.blade.php +++ b/resources/views/livewire/profile.blade.php @@ -1,9 +1,12 @@
-
-
-

{{ __('Profile') }}

+ +
diff --git a/resources/views/livewire/profile/memberships.blade.php b/resources/views/livewire/profile/memberships.blade.php new file mode 100644 index 0000000..1e3d006 --- /dev/null +++ b/resources/views/livewire/profile/memberships.blade.php @@ -0,0 +1,77 @@ +
+ + +
+
+
+ {{ __('profile.exportAsPdf') }} +
+
+
+ + +
+ + + + {{ __('profile.role') }} + + + {{ __('profile.committee') }} + + + {{ __('profile.from') }} + + + {{ __('profile.until') }} + + + {{ __('profile.decision') }} + + + {{ __('profile.comment') }} + + + @forelse($memberships as $row) + + + {{ $row['role']->getFirstAttribute('description') }} + + + {{ $row['role']->committee()->getFirstAttribute('description') }} + + + {{ \Carbon\Carbon::parse($row['from'])->format('Y-m-d') }} + + + @if ($row['until'] != '') + {{ \Carbon\Carbon::parse($row['until'])->format('Y-m-d') }} + @else + {{ __('profile.today') }} + @endif + + + {{ \Carbon\Carbon::parse($row['decided'])->format('Y-m-d') }} + + + {{ $row['comment'] }} + + + @empty + + +
+ {{ __('groups.no_roles_found') }} +
+
+
+ @endforelse +
+
+
diff --git a/resources/views/livewire/realm/community-dashboard.blade.php b/resources/views/livewire/realm/community-dashboard.blade.php index 5a8e532..108b696 100644 --- a/resources/views/livewire/realm/community-dashboard.blade.php +++ b/resources/views/livewire/realm/community-dashboard.blade.php @@ -4,7 +4,7 @@

{{ __('realms.dashboard.explanation', ['name' => $name]) }}

- + {{ __('realms.dashboard.profile_heading', ['name' => $name]) }} diff --git a/resources/views/livewire/realm/members.blade.php b/resources/views/livewire/realm/members.blade.php index 07addda..9ab717b 100644 --- a/resources/views/livewire/realm/members.blade.php +++ b/resources/views/livewire/realm/members.blade.php @@ -32,14 +32,31 @@ @forelse($realm_members as $realm_member) - {{ $realm_member->cn[0] }} - {{ $realm_member->uid[0] }} - {{ __('Remove Member') }} - + @if (auth()->user()->can('superadmin', \App\Ldap\User::class)) + + {{ $realm_member->getFirstAttribute('cn') }} + + @else + {{ $realm_member->getFirstAttribute('cn') }} + @endif + + {{ $realm_member->getFirstAttribute('uid') }} + +
+ + {{ __('profile.membershipsAsPdf') }} + + {{ __('Remove Member') }} + +
@empty diff --git a/resources/views/pdfs/memberships.blade.php b/resources/views/pdfs/memberships.blade.php new file mode 100644 index 0000000..d6b39c8 --- /dev/null +++ b/resources/views/pdfs/memberships.blade.php @@ -0,0 +1,122 @@ + + + + + + + + +

{{ __('profile.activities') }}

+

{{ $fullName }} @if($community)| {{ $community }} @endif| {{ date("Y-m-d") }}

+ + + + + + + + + + + + @foreach ($memberships as $row) + + + + + + + @endforeach + +
{{ __('profile.role') }}{{ __('profile.committee') }}{{ __('profile.from') }}{{ __('profile.until') }}
{{ $row['role']->getFirstAttribute('description') }}{{ $row['role']->committee()->getFirstAttribute('description') }}{{ \Carbon\Carbon::parse($row['from'])->format('Y-m-d') }} + @if ($row['until'] != '') + {{ \Carbon\Carbon::parse($row['until'])->format('Y-m-d') }} + @else + {{ __('profile.today') }} + @endif +
+ + \ No newline at end of file diff --git a/routes/auth.php b/routes/auth.php index 14e8251..85516c4 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -49,7 +49,7 @@ Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); - Route::any('change-password', ChangePassword::class) + Route::any('profile/{username}/password', ChangePassword::class) ->name('password.change'); Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index f46a667..0163e41 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -29,7 +29,12 @@ }); Breadcrumbs::for('profile', function (BreadcrumbTrail $trail, array $routeParams) { - $trail->push(__('Profile'), route('profile', $routeParams)); + $trail->push(__('Profile'), route('profile', array_merge(['username' => auth()->user()->username], $routeParams))); +}); + +Breadcrumbs::for('profile.memberships', function (BreadcrumbTrail $trail, array $routeParams) { + $trail->parent('profile', $routeParams); + $trail->push(__('profile.memberships'), route('profile.memberships', $routeParams)); }); Breadcrumbs::for('password.change', function (BreadcrumbTrail $trail, array $routeParams) { diff --git a/routes/web.php b/routes/web.php index 8549a0b..85cf419 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,7 +21,8 @@ Route::get('/', static function (){ return redirect()->route('realms.pick'); }); - Route::get('/profile', \App\Livewire\Profile::class)->name('profile'); + Route::get('/profile/{username}', \App\Livewire\Profile::class)->name('profile'); + Route::get('/profile/{username}/memberships', \App\Livewire\Profile\Memberships::class)->name('profile.memberships'); Route::get('/pick-realm', \App\Livewire\Realm\ListRealms::class)->name('realms.pick'); Route::middleware(['communityMember'])->group(function (){