Add avatars, profiles, and auth flows
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -19,8 +20,16 @@ class CreateNewUser implements CreatesNewUsers
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
$input['name_canonical'] = Str::lower(trim($input['name'] ?? ''));
|
||||
|
||||
Validator::make(data: $input, rules: [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'name_canonical' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique(table: User::class, column: 'name_canonical'),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
@@ -33,6 +42,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
|
||||
return User::create(attributes: [
|
||||
'name' => $input['name'],
|
||||
'name_canonical' => $input['name_canonical'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make(value: $input['password']),
|
||||
]);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
@@ -17,8 +18,16 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
$input['name_canonical'] = Str::lower(trim($input['name'] ?? ''));
|
||||
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'name_canonical' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('users', 'name_canonical')->ignore($user->id),
|
||||
],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
@@ -34,6 +43,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
} else {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'name_canonical' => $input['name_canonical'],
|
||||
'email' => $input['email'],
|
||||
])->save();
|
||||
}
|
||||
@@ -48,6 +58,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'name_canonical' => $input['name_canonical'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\PasswordValidationRules;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
public function register(Request $request, CreateNewUser $creator): JsonResponse
|
||||
{
|
||||
$input = [
|
||||
@@ -33,16 +41,30 @@ class AuthController extends Controller
|
||||
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$request->merge([
|
||||
'login' => $request->input('login', $request->input('email')),
|
||||
]);
|
||||
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'login' => ['required', 'string'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $request->input('email'))->first();
|
||||
$login = trim((string) $request->input('login'));
|
||||
$loginNormalized = Str::lower($login);
|
||||
$userQuery = User::query();
|
||||
|
||||
if (filter_var($login, FILTER_VALIDATE_EMAIL)) {
|
||||
$userQuery->whereRaw('lower(email) = ?', [$loginNormalized]);
|
||||
} else {
|
||||
$userQuery->where('name_canonical', $loginNormalized);
|
||||
}
|
||||
|
||||
$user = $userQuery->first();
|
||||
|
||||
if (!$user || !Hash::check($request->input('password'), $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['Invalid credentials.'],
|
||||
'login' => ['Invalid credentials.'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -62,6 +84,93 @@ class AuthController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function verifyEmail(Request $request, string $id, string $hash): RedirectResponse
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
if (!hash_equals($hash, sha1($user->getEmailForVerification()))) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (!$user->hasVerifiedEmail()) {
|
||||
$user->markEmailAsVerified();
|
||||
event(new Verified($user));
|
||||
}
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
public function forgotPassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
if ($status !== Password::RESET_LINK_SENT) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [__($status)],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => __($status)]);
|
||||
}
|
||||
|
||||
public function resetPassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => $this->passwordRules(),
|
||||
]);
|
||||
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function (User $user, string $password) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
if ($status !== Password::PASSWORD_RESET) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [__($status)],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => __($status)]);
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'current_password' => ['required'],
|
||||
'password' => $this->passwordRules(),
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user || !Hash::check($request->input('current_password'), $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'current_password' => ['Invalid current password.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->input('password')),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
return response()->json(['message' => 'Password updated.']);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()?->currentAccessToken()?->delete();
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Post;
|
||||
use App\Models\Thread;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class PostController extends Controller
|
||||
{
|
||||
@@ -84,6 +85,9 @@ class PostController extends Controller
|
||||
'thread' => "/api/threads/{$post->thread_id}",
|
||||
'user_id' => $post->user_id,
|
||||
'user_name' => $post->user?->name,
|
||||
'user_avatar_url' => $post->user?->avatar_path
|
||||
? Storage::url($post->user->avatar_path)
|
||||
: null,
|
||||
'created_at' => $post->created_at?->toIso8601String(),
|
||||
'updated_at' => $post->updated_at?->toIso8601String(),
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Forum;
|
||||
use App\Models\Thread;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ThreadController extends Controller
|
||||
{
|
||||
@@ -95,6 +96,9 @@ class ThreadController extends Controller
|
||||
'forum' => "/api/forums/{$thread->forum_id}",
|
||||
'user_id' => $thread->user_id,
|
||||
'user_name' => $thread->user?->name,
|
||||
'user_avatar_url' => $thread->user?->avatar_path
|
||||
? Storage::url($thread->user->avatar_path)
|
||||
: null,
|
||||
'created_at' => $thread->created_at?->toIso8601String(),
|
||||
'updated_at' => $thread->updated_at?->toIso8601String(),
|
||||
];
|
||||
|
||||
@@ -8,6 +8,31 @@ use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UploadController extends Controller
|
||||
{
|
||||
public function storeAvatar(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'file' => ['required', 'image', 'mimes:jpg,jpeg,png,gif,webp', 'max:2048'],
|
||||
]);
|
||||
|
||||
if ($user->avatar_path) {
|
||||
Storage::disk('public')->delete($user->avatar_path);
|
||||
}
|
||||
|
||||
$path = $data['file']->store('avatars', 'public');
|
||||
$user->avatar_path = $path;
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'path' => $path,
|
||||
'url' => Storage::url($path),
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeLogo(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
@@ -17,9 +19,46 @@ class UserController extends Controller
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => $this->resolveAvatarUrl($user),
|
||||
'roles' => $user->roles->pluck('name')->values(),
|
||||
]);
|
||||
|
||||
return response()->json($users);
|
||||
}
|
||||
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['message' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => $this->resolveAvatarUrl($user),
|
||||
'roles' => $user->roles()->pluck('name')->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function profile(User $user): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'avatar_url' => $this->resolveAvatarUrl($user),
|
||||
'created_at' => $user->created_at?->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAvatarUrl(User $user): ?string
|
||||
{
|
||||
if (!$user->avatar_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Storage::url($user->avatar_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'name_canonical',
|
||||
'avatar_path',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user