generated from thegrind/laravel-dockerized
Bunch of progress
This commit is contained in:
parent
9746756a44
commit
38c0c70ded
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@ yarn-error.log
|
||||
/.vscode
|
||||
/.zed
|
||||
/storage/oauth/*
|
||||
/storage/avatars/*
|
@ -172,7 +172,8 @@ class OIDCController extends Controller
|
||||
'sub' => (string) $user->id,
|
||||
'email' => $user->email,
|
||||
'name' => $user->name,
|
||||
'preferred_username' => str($user->name)->slug()->toString(),
|
||||
'preferred_username' => $user->preferred_username,
|
||||
'picture' => $user->avatar ? $user->avatarUrl() : null
|
||||
]);
|
||||
}
|
||||
|
||||
@ -206,7 +207,14 @@ class OIDCController extends Controller
|
||||
'scopes_supported' => ["openid", "profile", "email"],
|
||||
'response_types_supported' => ["code"],
|
||||
"jwks_uri" => route('auth.keys'),
|
||||
"id_token_signing_alg_values_supported" => ["RS256"]
|
||||
"id_token_signing_alg_values_supported" => ["RS256"],
|
||||
'claims_supported' => [
|
||||
'sub',
|
||||
'email',
|
||||
'name',
|
||||
'preferred_username',
|
||||
'picture'
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
93
app/Livewire/Forms/UserProfile.php
Normal file
93
app/Livewire/Forms/UserProfile.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class UserProfile extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
// Profile info
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
public ?string $preferred_username = null;
|
||||
public string $avatar = '';
|
||||
#[Validate('image|max:10000')]
|
||||
public $avatarUpload;
|
||||
// Password
|
||||
public string $current_password = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->name = Auth::user()->name;
|
||||
$this->email = Auth::user()->email;
|
||||
$this->preferred_username = Auth::user()->preferred_username;
|
||||
$this->avatar = Auth::user()->avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the profile information for the currently authenticated user.
|
||||
*/
|
||||
public function updateProfileInformation(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$validated = $this->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($user->id),
|
||||
],
|
||||
'preferred_username' => 'string|max:255'
|
||||
]);
|
||||
|
||||
$user->fill($validated);
|
||||
|
||||
if (!empty($this->avatarUpload)) {
|
||||
if (!empty($user->avatar)) {
|
||||
Storage::disk('avatars')->delete($user->avatar);
|
||||
}
|
||||
$user->avatar = $this->avatarUpload->store(options: 'avatars');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
$this->dispatch('profile-updated', name: $user->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email verification notification to the current user.
|
||||
*/
|
||||
public function resendVerificationNotification(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
|
||||
Session::flash('status', 'verification-link-sent');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.forms.user-profile');
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ use Livewire\Component;
|
||||
class Profile extends Component
|
||||
{
|
||||
public string $name = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
/**
|
||||
|
@ -23,6 +23,8 @@ class User extends Authenticatable
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'avatar',
|
||||
'preferred_username'
|
||||
];
|
||||
|
||||
/**
|
||||
@ -64,4 +66,9 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->hasMany(AuthenticationToken::class);
|
||||
}
|
||||
|
||||
public function avatarUrl()
|
||||
{
|
||||
return route('user.avatar', ['path' => $this->avatar]);
|
||||
}
|
||||
}
|
||||
|
@ -41,12 +41,21 @@ return [
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'url' => env('APP_URL') . '/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'avatars' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('avatars'),
|
||||
'url' => env('APP_URL') . '/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => true,
|
||||
'report' => true,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('avatar')->nullable()->after('email');
|
||||
$table->string('preferred_username')->nullable()->after('avatar');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('avatar');
|
||||
$table->dropColumn('preferred_username');
|
||||
});
|
||||
}
|
||||
};
|
@ -57,8 +57,10 @@
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}
|
||||
</flux:menu.item>
|
||||
<flux:modal.trigger name="user-profile">
|
||||
<flux:menu.item>{{ __('Edit your profile') }}
|
||||
</flux:menu.item>
|
||||
</flux:modal.trigger>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
@ -1,3 +1,3 @@
|
||||
<x-layouts.auth.simple :title="$title ?? null">
|
||||
<x-layouts.auth.split :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts.auth.simple>
|
||||
</x-layouts.auth.split>
|
@ -1,43 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
|
||||
<div class="absolute inset-0 bg-neutral-900"></div>
|
||||
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
|
||||
</span>
|
||||
{{ config('app.name', 'Laravel') }}
|
||||
</a>
|
||||
|
||||
@php
|
||||
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
|
||||
@endphp
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
|
||||
<div class="relative z-20 mt-auto">
|
||||
<blockquote class="space-y-2">
|
||||
<flux:heading size="lg">“{{ trim($message) }}”</flux:heading>
|
||||
<footer><flux:heading>{{ trim($author) }}</flux:heading></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:p-8">
|
||||
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div
|
||||
class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div
|
||||
class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
|
||||
<div style="background: url('/img/background.jpg') no-repeat center center; background-size: cover;"
|
||||
class="absolute inset-0 bg-neutral-900"></div>
|
||||
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
|
||||
</span>
|
||||
{{ config('app.name', 'Laravel') }}
|
||||
</a>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@php
|
||||
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
|
||||
@endphp
|
||||
|
||||
<div class="relative z-20 mt-auto">
|
||||
<blockquote class="space-y-2">
|
||||
<flux:heading size="lg">“{{ trim($message) }}”</flux:heading>
|
||||
<footer>
|
||||
<flux:heading>{{ trim($author) }}</flux:heading>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
@fluxScripts
|
||||
</body>
|
||||
<div class="w-full lg:p-8">
|
||||
|
||||
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden"
|
||||
wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@fluxScripts
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,5 +1,8 @@
|
||||
<x-layouts.app :title="__('Dashboard')">
|
||||
<div class="max-w-4xl mx-auto py-12">
|
||||
<livewire:app-container />
|
||||
<div class="mt-4">
|
||||
<livewire:forms.user-profile />
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
90
resources/views/livewire/forms/user-profile.blade.php
Normal file
90
resources/views/livewire/forms/user-profile.blade.php
Normal file
@ -0,0 +1,90 @@
|
||||
<div>
|
||||
<div x-data="{edit: false}" class="border rounded-sm dark:border-zinc-600 border-stone-300 p-4">
|
||||
<div x-show="!edit" class="flex gap-4">
|
||||
<div>
|
||||
@if (!empty($avatarUpload))
|
||||
<img src="{{$avatarUpload->temporaryUrl()}}" alt="" class="size-20">
|
||||
@elseif(!empty($avatar))
|
||||
<img src="{{auth()->user()->avatarUrl()}}" alt="" class="size-20">
|
||||
@else
|
||||
<div
|
||||
class="size-20 flex items-center justify-center mx-auto rounded-sm dark:bg-zinc-600 bg-stone-300 dark:text-white text-black">
|
||||
<div class="text-4xl font-bold">{{auth()->user()->initials()}}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-between">
|
||||
<flux:text>{{auth()->user()->name}}</flux:text>
|
||||
<flux:text>{{auth()->user()->preferred_username}} <span class="italic">(preferred username)</span>
|
||||
</flux:text>
|
||||
<flux:text>{{auth()->user()->email}}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
<form x-show="edit" wire:submit="updateProfileInformation" x-on:submit="edit = false"
|
||||
class="my-6 w-full space-y-6">
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="relative">
|
||||
<label for="avatar-upload"
|
||||
class="flex items-center justify-center absolute bg-black opacity-0 cursor-pointer hover:opacity-80 top-0 bottom-0 left-0 right-0">
|
||||
<flux:icon.pencil class="size-12" />
|
||||
</label>
|
||||
@if (!empty($avatarUpload))
|
||||
<img src="{{$avatarUpload->temporaryUrl()}}" alt="" class="size-32">
|
||||
@elseif(!empty($avatar))
|
||||
<img src="{{auth()->user()->avatarUrl()}}" alt="" class="size-32">
|
||||
@else
|
||||
<div
|
||||
class="size-32 flex items-center justify-center mx-auto rounded-sm dark:bg-zinc-600 bg-stone-300 dark:text-white text-black">
|
||||
<div class="text-4xl font-bold">{{auth()->user()->initials()}}</div>
|
||||
</div>
|
||||
@endif
|
||||
<flux:input type="file" id="avatar-upload" wire:model="avatarUpload" class="hidden" />
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col gap-4">
|
||||
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus
|
||||
autocomplete="name" />
|
||||
<flux:input wire:model="preferred_username" :label="__('Preferred Username')" type="text" required
|
||||
autofocus autocomplete="username" />
|
||||
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
<flux:button variant="subtle" x-on:click="edit = false">Cancel</flux:button>
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="profile-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4">
|
||||
<flux:button variant="subtle" icon="pencil" x-on:click="edit = true" inset>Edit Profile information
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
<div x-data="{edit: false}" class="mt-4">
|
||||
<flux:button variant="primary" x-show="!edit" x-on:click="edit = true">Change Password</flux:button>
|
||||
<form wire:submit="updatePassword" class="mt-6 space-y-6" x-show="edit">
|
||||
<flux:input wire:model="current_password" :label="__('Current password')" type="password" required
|
||||
autocomplete="current-password" />
|
||||
<flux:input wire:model="password" :label="__('New password')" type="password" required
|
||||
autocomplete="new-password" />
|
||||
<flux:input wire:model="password_confirmation" :label="__('Confirm Password')" type="password" required
|
||||
autocomplete="new-password" />
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
<flux:button variant="subtle" x-on:click="edit = false">Cancel</flux:button>
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="password-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -5,8 +5,10 @@ use App\Livewire\ConsentScreen;
|
||||
use App\Livewire\Settings\Appearance;
|
||||
use App\Livewire\Settings\Password;
|
||||
use App\Livewire\Settings\Profile;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
@ -24,6 +26,12 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::get('settings/appearance', Appearance::class)->name('settings.appearance');
|
||||
});
|
||||
|
||||
Route::get('avatars/{path}', function (string $path) {
|
||||
$path = Storage::disk('avatars')->path($path);
|
||||
|
||||
return response()->file($path);
|
||||
})->name('user.avatar');
|
||||
|
||||
// OIDC Endpoints
|
||||
Route::prefix('application/o')->group(function () {
|
||||
Route::get('authorize', [OIDCController::class, 'authorize'])->middleware('auth')->name('auth.authorize');
|
||||
|
@ -3,6 +3,8 @@ import {
|
||||
} from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
@ -14,6 +16,14 @@ export default defineConfig({
|
||||
],
|
||||
server: {
|
||||
cors: true,
|
||||
host: "homelab-sso.test"
|
||||
host: "homelab-sso.test",
|
||||
// https: {
|
||||
// key: fs.readFileSync(path.resolve(
|
||||
// process.env.HOME, '.valet/Certificates/homelab-sso.test.key'
|
||||
// )),
|
||||
// cert: fs.readFileSync(path.resolve(
|
||||
// process.env.HOME, '.valet/Certificates/homelab-sso.test.crt'
|
||||
// )),
|
||||
// },
|
||||
},
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user