User management (#185)

This commit is contained in:
Saeed Vaziry
2024-04-29 20:58:04 +02:00
committed by GitHub
parent 35f896eab1
commit d846acaa8d
106 changed files with 1490 additions and 434 deletions

View File

@ -1,5 +1,5 @@
<x-profile-layout>
<x-settings-layout>
<x-slot name="pageTitle">{{ __("Notification Channels") }}</x-slot>
@include("settings.notification-channels.partials.channels-list")
</x-profile-layout>
</x-settings-layout>

View File

@ -10,7 +10,7 @@
<form
id="add-channel-form"
hx-post="{{ route("notification-channels.add") }}"
hx-post="{{ route("settings.notification-channels.add") }}"
hx-swap="outerHTML"
hx-select="#add-channel-form"
hx-ext="disable-element"

View File

@ -24,7 +24,7 @@
<div class="flex items-center">
<div class="inline">
<x-icon-button
x-on:click="deleteAction = '{{ route('notification-channels.delete', $channel->id) }}'; $dispatch('open-modal', 'delete-channel')"
x-on:click="deleteAction = '{{ route('settings.notification-channels.delete', $channel->id) }}'; $dispatch('open-modal', 'delete-channel')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>

View File

@ -1,9 +0,0 @@
<x-profile-layout>
<x-slot name="pageTitle">{{ __("Profile") }}</x-slot>
@include("settings.profile.partials.update-profile-information")
@include("settings.profile.partials.update-password")
@include("settings.profile.partials.two-factor-authentication")
</x-profile-layout>

View File

@ -1,60 +0,0 @@
<x-card>
<x-slot name="title">
{{ __("Two Factor Authentication") }}
</x-slot>
<x-slot name="description">
{{ __("Here you can activate 2FA to secure your account") }}
</x-slot>
@if (! auth()->user()->two_factor_secret)
{{-- Enable 2FA --}}
<form method="POST" action="{{ route("two-factor.enable") }}">
@csrf
<x-primary-button type="submit">
{{ __("Enable Two-Factor") }}
</x-primary-button>
</form>
@else
{{-- Disable 2FA --}}
<form method="POST" action="{{ route("two-factor.disable") }}">
@csrf
@method("DELETE")
<x-danger-button type="submit">
{{ __("Disable Two-Factor") }}
</x-danger-button>
</form>
@if (session("status") == "two-factor-authentication-enabled")
<div class="mt-5">
{{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }}
</div>
<div class="mt-5">
{!! auth()->user()->twoFactorQrCodeSvg() !!}
</div>
@endif
{{-- Show 2FA Recovery Codes --}}
<div class="mt-5">
{{ __("Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.") }}
</div>
<div class="mt-5 rounded-md border border-gray-100 p-2 dark:border-gray-700">
@foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code)
<div class="mt-2">{{ $code }}</div>
@endforeach
</div>
{{-- Regenerate 2FA Recovery Codes --}}
<form class="mt-5" method="POST" action="{{ route("two-factor.recovery-codes") }}">
@csrf
<x-primary-button type="submit">
{{ __("Regenerate Recovery Codes") }}
</x-primary-button>
</form>
@endif
</x-card>

View File

@ -1,80 +0,0 @@
@php
$user = auth()->user();
@endphp
<x-card>
<x-slot name="title">
{{ __("Profile Information") }}
</x-slot>
<x-slot name="description">
{{ __("Update your account's profile information and email address.") }}
</x-slot>
<form
id="update-profile-information"
hx-post="{{ route("profile.info") }}"
hx-swap="outerHTML"
hx-select="#update-profile-information"
hx-trigger="submit"
hx-ext="disable-element"
hx-disable-element="#btn-save-info"
class="mt-6 space-y-6"
>
@csrf
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input
id="name"
name="name"
type="text"
value="{{ old('name', auth()->user()->name) }}"
class="mt-1 block w-full"
required
autocomplete="name"
/>
@error("name")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input
id="email"
name="email"
type="email"
value="{{ old('email', auth()->user()->email) }}"
class="mt-1 block w-full"
required
autocomplete="email"
/>
@error("email")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="timezone" :value="__('Timezone')" />
<x-select-input id="timezone" name="timezone" class="mt-1 block w-full" required>
@foreach (timezone_identifiers_list() as $timezone)
<option
value="{{ $timezone }}"
@if(old('timezone', auth()->user()->timezone) == $timezone) selected @endif
>
{{ $timezone }}
</option>
@endforeach
</x-select-input>
@error("timezone")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</form>
<x-slot name="actions">
<x-primary-button id="btn-save-info" form="update-profile-information">
{{ __("Save") }}
</x-primary-button>
</x-slot>
</x-card>

View File

@ -1,5 +1,5 @@
<x-profile-layout>
<x-settings-layout>
<x-slot name="pageTitle">{{ __("Projects") }}</x-slot>
@include("settings.projects.partials.projects-list")
</x-profile-layout>
</x-settings-layout>

View File

@ -6,7 +6,7 @@
<x-modal name="create-project" :show="request()->has('create')">
<form
id="create-project-form"
hx-post="{{ route("projects.create") }}"
hx-post="{{ route("settings.projects.create") }}"
hx-swap="outerHTML"
hx-select="#create-project-form"
hx-ext="disable-element"

View File

@ -6,7 +6,7 @@
<x-modal name="edit-project-{{ $project->id }}">
<form
id="edit-project-form-{{ $project->id }}"
hx-post="{{ route("projects.update", $project) }}"
hx-post="{{ route("settings.projects.update", $project) }}"
hx-swap="outerHTML"
hx-select="#edit-project-form-{{ $project->id }}"
hx-ext="disable-element"

View File

@ -27,7 +27,7 @@
<div class="flex items-center">
@include("settings.projects.partials.edit-project", ["project" => $project])
<x-icon-button
x-on:click="deleteAction = '{{ route('projects.delete', $project) }}'; $dispatch('open-modal', 'delete-project')"
x-on:click="deleteAction = '{{ route('settings.projects.delete', $project) }}'; $dispatch('open-modal', 'delete-project')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>

View File

@ -1,5 +1,5 @@
<x-profile-layout>
<x-settings-layout>
<x-slot name="pageTitle">{{ __("Storage Providers") }}</x-slot>
@include("settings.server-providers.partials.providers-list")
</x-profile-layout>
</x-settings-layout>

View File

@ -10,7 +10,7 @@
<form
id="connect-provider-form"
hx-post="{{ route("server-providers.connect") }}"
hx-post="{{ route("settings.server-providers.connect") }}"
hx-swap="outerHTML"
hx-select="#connect-provider-form"
hx-ext="disable-element"

View File

@ -26,7 +26,7 @@ class="h-10 w-10"
<div class="flex items-center">
<div class="inline">
<x-icon-button
x-on:click="deleteAction = '{{ route('server-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')"
x-on:click="deleteAction = '{{ route('settings.server-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>

View File

@ -1,5 +1,5 @@
<x-profile-layout>
<x-settings-layout>
<x-slot name="pageTitle">{{ __("Source Controls") }}</x-slot>
@include("settings.source-controls.partials.source-controls-list")
</x-profile-layout>
</x-settings-layout>

View File

@ -10,7 +10,7 @@
<form
id="connect-source-control-form"
hx-post="{{ route("source-controls.connect") }}"
hx-post="{{ route("settings.source-controls.connect") }}"
hx-swap="outerHTML"
hx-select="#connect-source-control-form"
hx-ext="disable-element"

View File

@ -22,7 +22,7 @@
<div class="flex items-center">
<div class="inline">
<x-icon-button
x-on:click="deleteAction = '{{ route('source-controls.delete', $sourceControl->id) }}'; $dispatch('open-modal', 'delete-source-control')"
x-on:click="deleteAction = '{{ route('settings.source-controls.delete', $sourceControl->id) }}'; $dispatch('open-modal', 'delete-source-control')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>

View File

@ -1,5 +1,5 @@
<x-profile-layout>
<x-settings-layout>
<x-slot name="pageTitle">{{ __("SSH Keys") }}</x-slot>
@include("settings.ssh-keys.partials.keys-list")
</x-profile-layout>
</x-settings-layout>

View File

@ -6,7 +6,7 @@
<x-modal name="add-key">
<form
id="add-ssh-key-form"
hx-post="{{ route("ssh-keys.add") }}"
hx-post="{{ route("settings.ssh-keys.add") }}"
hx-swap="outerHTML"
hx-select="#add-ssh-key-form"
hx-ext="disable-element"

View File

@ -21,7 +21,7 @@
<div class="flex items-center">
<div class="inline">
<x-icon-button
x-on:click="deleteAction = '{{ route('ssh-keys.delete', $key->id) }}'; $dispatch('open-modal', 'delete-ssh-key')"
x-on:click="deleteAction = '{{ route('settings.ssh-keys.delete', $key->id) }}'; $dispatch('open-modal', 'delete-ssh-key')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>

View File

@ -1,5 +1,5 @@
<x-profile-layout>
<x-settings-layout>
<x-slot name="pageTitle">{{ __("Storage Providers") }}</x-slot>
@include("settings.storage-providers.partials.providers-list")
</x-profile-layout>
</x-settings-layout>

View File

@ -10,7 +10,7 @@
<form
id="connect-storage-provider-form"
hx-post="{{ route("storage-providers.connect") }}"
hx-post="{{ route("settings.storage-providers.connect") }}"
hx-swap="outerHTML"
hx-select="#connect-storage-provider-form"
hx-ext="disable-element"

View File

@ -43,7 +43,7 @@ class="h-10 w-10"
<div class="flex items-center">
<div class="inline">
<x-icon-button
x-on:click="deleteAction = '{{ route('storage-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')"
x-on:click="deleteAction = '{{ route('settings.storage-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>

View File

@ -0,0 +1,61 @@
<x-settings-layout>
<x-slot name="pageTitle">Users</x-slot>
<x-container>
<x-card-header>
<x-slot name="title">Users</x-slot>
<x-slot name="description">Here you can manage users</x-slot>
<x-slot name="aside">
@include("settings.users.partials.create-user")
</x-slot>
</x-card-header>
<div class="space-y-3" x-data="{ deleteAction: '' }">
<x-table>
<x-thead>
<x-tr>
<x-th>ID</x-th>
<x-th>Name</x-th>
<x-th>Email</x-th>
<x-th>Role</x-th>
<x-th></x-th>
</x-tr>
</x-thead>
<x-tbody>
@foreach ($users as $user)
<x-tr>
<x-td>{{ $user->id }}</x-td>
<x-td>{{ $user->name }}</x-td>
<x-td>{{ $user->email }}</x-td>
<x-td>
<div class="inline-flex">
@if ($user->role === \App\Enums\UserRole::ADMIN)
<x-status status="success">ADMIN</x-status>
@else
<x-status status="info">USER</x-status>
@endif
</div>
</x-td>
<x-td class="text-right">
<x-icon-button
x-on:click="deleteAction = '{{ route('settings.users.delete', ['user' => $user]) }}'; $dispatch('open-modal', 'delete-user')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>
<x-icon-button :href="route('settings.users.show', ['user' => $user])">
<x-heroicon name="o-cog-6-tooth" class="h-5 w-5" />
</x-icon-button>
</x-td>
</x-tr>
@endforeach
</x-tbody>
</x-table>
<x-confirmation-modal
name="delete-user"
:title="__('Confirm')"
:description="__('Are you sure that you want to delete this user?')"
method="delete"
x-bind:action="deleteAction"
/>
</div>
</x-container>
</x-settings-layout>

View File

@ -0,0 +1,69 @@
<div>
<x-primary-button x-data="" x-on:click.prevent="$dispatch('open-modal', 'create-user')">New User</x-primary-button>
<x-modal name="create-user">
<form
id="create-user-form"
hx-post="{{ route("settings.users.store") }}"
hx-swap="outerHTML"
hx-select="#create-user-form"
hx-ext="disable-element"
hx-disable-element="#btn-create-user"
class="p-6"
>
@csrf
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Create New User</h2>
<div class="mt-6">
<x-input-label for="name" value="Name" />
<x-text-input value="{{ old('name') }}" id="name" name="name" type="text" class="mt-1 w-full" />
@error("name")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="email" value="Email" />
<x-text-input value="{{ old('email') }}" id="email" name="email" type="text" class="mt-1 w-full" />
@error("email")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="password" value="Password" />
<x-text-input id="password" name="password" type="password" class="mt-1 w-full" />
@error("password")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="role" value="Role" />
<x-select-input id="role" name="role" class="mt-1 w-full">
<option
value="{{ \App\Enums\UserRole::USER }}"
@if(old('role') === \App\Enums\UserRole::USER) selected @endif
>
User
</option>
<option
value="{{ \App\Enums\UserRole::ADMIN }}"
@if(old('role') === \App\Enums\UserRole::ADMIN) selected @endif
>
Admin
</option>
</x-select-input>
@error("role")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">Cancel</x-secondary-button>
<x-primary-button id="btn-create-project" class="ml-3">Create</x-primary-button>
</div>
</form>
</x-modal>
</div>

View File

@ -0,0 +1,120 @@
<x-card>
<x-slot name="title">Projects</x-slot>
<x-slot name="description">Manage the projects that the user is in</x-slot>
<x-slot name="aside">
<x-secondary-button :href="route('settings.users.index')">Back to Users</x-secondary-button>
</x-slot>
<form
id="update-projects"
hx-post="{{ route("settings.users.update-projects", ["user" => $user]) }}"
hx-swap="outerHTML"
hx-select="#update-projects"
hx-trigger="submit"
hx-ext="disable-element"
hx-disable-element="#btn-save-projects"
class="mt-6"
>
@csrf
<script>
let projects = @json($user->projects);
</script>
<div
class="space-y-6"
x-data="{
q: '',
projects: projects,
search() {
htmx.ajax('GET', '{{ request()->getUri() }}?q=' + this.q, {
target: '#projects-list',
swap: 'outerHTML',
select: '#projects-list',
}).then(() => {
document.getElementById('q').focus()
})
},
addProject(project) {
if (this.projects.find((p) => p.id === project.id)) {
return
}
this.projects.push(project)
this.q = ''
},
removeProject(id) {
this.projects = this.projects.filter((project) => project.id !== id)
},
}"
>
<div>
<x-input-label value="Projects" />
<div class="mt-1">
<template x-for="project in projects">
<div class="mr-1 inline-flex">
<x-status status="info" class="flex items-center">
<span x-text="project.name"></span>
<x-heroicon
name="o-x-mark"
class="ml-1 h-4 w-4 cursor-pointer"
x-on:click="removeProject(project.id)"
/>
<input type="hidden" name="projects[]" x-bind:value="project.id" />
</x-status>
</div>
</template>
</div>
</div>
<div>
<x-input-label value="Add new Project" />
@php
$projects = \App\Models\Project::query()
->where(function ($query) {
if (request()->has("q")) {
$query->where("name", "like", "%" . request("q") . "%");
}
})
->take(5)
->get();
@endphp
<x-dropdown width="full">
<x-slot name="trigger">
<x-text-input
id="q"
name="q"
x-model="q"
type="text"
class="mt-1 w-full"
placeholder="Search for projects..."
autocomplete="off"
x-on:input.debounce.500ms="search"
/>
</x-slot>
<x-slot name="content">
<div id="projects-list">
@foreach ($projects as $project)
<x-dropdown-link
class="cursor-pointer"
x-on:click="addProject({ id: {{ $project->id }}, name: '{{ $project->name }}' })"
>
{{ $project->name }}
</x-dropdown-link>
@endforeach
</div>
</x-slot>
</x-dropdown>
</div>
</div>
</form>
<x-slot name="actions">
<x-primary-button id="btn-save-projects" form="update-projects">Save</x-primary-button>
</x-slot>
</x-card>

View File

@ -0,0 +1,99 @@
<x-card>
<x-slot name="title">User Info</x-slot>
<x-slot name="description">You can update user's info here</x-slot>
<form
id="update-user-info"
hx-post="{{ route("settings.users.update", ["user" => $user]) }}"
hx-swap="outerHTML"
hx-select="#update-user-info"
hx-trigger="submit"
hx-ext="disable-element"
hx-disable-element="#btn-save-info"
class="mt-6 space-y-6"
>
@csrf
<div>
<x-input-label for="name" value="Name" />
<x-text-input
id="name"
name="name"
type="text"
value="{{ old('name', $user->name) }}"
class="mt-1 block w-full"
required
autocomplete="name"
/>
@error("name")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="email" value="Email" />
<x-text-input
id="email"
name="email"
type="email"
value="{{ old('email', $user->email) }}"
class="mt-1 block w-full"
required
autocomplete="email"
/>
@error("email")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="timezone" value="Timezone" />
<x-select-input id="timezone" name="timezone" class="mt-1 block w-full" required>
@foreach (timezone_identifiers_list() as $timezone)
<option
value="{{ $timezone }}"
@if(old('timezone', $user->timezone) == $timezone) selected @endif
>
{{ $timezone }}
</option>
@endforeach
</x-select-input>
@error("timezone")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="role" value="Role" />
<x-select-input id="role" name="role" class="mt-1 w-full">
<option
value="{{ \App\Enums\UserRole::USER }}"
@if(old('role', $user->role) === \App\Enums\UserRole::USER) selected @endif
>
User
</option>
<option
value="{{ \App\Enums\UserRole::ADMIN }}"
@if(old('role', $user->role) === \App\Enums\UserRole::ADMIN) selected @endif
>
Admin
</option>
</x-select-input>
@error("role")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="password" value="New Password" />
<x-text-input id="password" name="password" type="password" class="mt-1 w-full" />
@error("password")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</form>
<x-slot name="actions">
<x-primary-button id="btn-save-info" form="update-user-info">Save</x-primary-button>
</x-slot>
</x-card>

View File

@ -0,0 +1,9 @@
<x-settings-layout>
<x-slot name="pageTitle">Users</x-slot>
<x-container>
@include("settings.users.partials.update-projects")
@include("settings.users.partials.update-user-info")
</x-container>
</x-settings-layout>