diff --git a/.gitignore b/.gitignore
index 7d71d59..3601ded 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,5 @@ yarn-error.log
/.nova
/.vscode
/.zed
-/storage/oauth/*
\ No newline at end of file
+/storage/oauth/*
+/storage/avatars/*
\ No newline at end of file
diff --git a/app/Http/Controllers/OIDCController.php b/app/Http/Controllers/OIDCController.php
index 9e73e01..8c35a36 100644
--- a/app/Http/Controllers/OIDCController.php
+++ b/app/Http/Controllers/OIDCController.php
@@ -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'
+ ]
]);
}
}
diff --git a/app/Livewire/Forms/UserProfile.php b/app/Livewire/Forms/UserProfile.php
new file mode 100644
index 0000000..26bd403
--- /dev/null
+++ b/app/Livewire/Forms/UserProfile.php
@@ -0,0 +1,93 @@
+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');
+ }
+}
diff --git a/app/Livewire/Settings/Profile.php b/app/Livewire/Settings/Profile.php
index c823a69..b0a65ee 100644
--- a/app/Livewire/Settings/Profile.php
+++ b/app/Livewire/Settings/Profile.php
@@ -11,7 +11,6 @@ use Livewire\Component;
class Profile extends Component
{
public string $name = '';
-
public string $email = '';
/**
diff --git a/app/Models/User.php b/app/Models/User.php
index cc13521..71cafdf 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -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]);
+ }
}
diff --git a/config/filesystems.php b/config/filesystems.php
index 3d671bd..42838a2 100644
--- a/config/filesystems.php
+++ b/config/filesystems.php
@@ -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'),
diff --git a/database/migrations/2025_07_27_215631_add_avatar_to_users.php b/database/migrations/2025_07_27_215631_add_avatar_to_users.php
new file mode 100644
index 0000000..8a81a03
--- /dev/null
+++ b/database/migrations/2025_07_27_215631_add_avatar_to_users.php
@@ -0,0 +1,30 @@
+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');
+ });
+ }
+};
diff --git a/resources/views/components/layouts/app/header.blade.php b/resources/views/components/layouts/app/header.blade.php
index 4150d82..015d0d0 100644
--- a/resources/views/components/layouts/app/header.blade.php
+++ b/resources/views/components/layouts/app/header.blade.php
@@ -57,8 +57,10 @@