createRegisteredUser($request); if ($user instanceof JsonResponse) { return $user; } $this->startWebSession($request, $user); return response()->json([ 'message' => __('api.auth.registered_unverified'), 'user' => new UserResource($user), ], 201); } public function login(LoginRequest $request): JsonResponse { $data = $request->validated(); $user = $this->getLoginUser($data); if ($user instanceof JsonResponse) { return $user; } $this->startWebSession($request, $user); return response()->json([ 'user' => new UserResource($user), ]); } public function mobileRegister(RegisterRequest $request): JsonResponse { $user = $this->createRegisteredUser($request); if ($user instanceof JsonResponse) { return $user; } return response()->json([ 'message' => __('api.auth.registered_unverified'), 'token' => $this->createMobileToken($user, $request->validated('device_name')), 'user' => new UserResource($user), ], 201); } public function mobileLogin(LoginRequest $request): JsonResponse { $data = $request->validated(); $user = $this->getLoginUser($data); if ($user instanceof JsonResponse) { return $user; } return response()->json([ 'token' => $this->createMobileToken($user, $data['device_name'] ?? null), 'user' => new UserResource($user), ]); } public function verifyEmail(Request $request, string $id, string $hash): JsonResponse|View { $user = User::findOrFail($id); if (! hash_equals($hash, sha1($user->getEmailForVerification()))) { if ($request->expectsJson()) { return response()->json([ 'message' => __('api.auth.invalid_verification_link'), ], 403); } abort(403, __('api.auth.invalid_verification_link')); } $alreadyVerified = $user->hasVerifiedEmail(); if (! $alreadyVerified) { $user->markEmailAsVerified(); event(new Verified($user)); } if ($request->expectsJson()) { return response()->json([ 'message' => $alreadyVerified ? __('api.auth.already_verified') : __('api.auth.verified'), 'user' => new UserResource($user->fresh()), ]); } return view('auth.email-verified', [ 'alreadyVerified' => $alreadyVerified, 'appName' => config('app.name'), ]); } public function sendVerificationEmail(EmailVerificationNotificationRequest $request): JsonResponse { $email = strtolower($request->validated('email')); $user = User::where('email', $email)->first(); if ($user && ! $user->hasVerifiedEmail()) { $user->sendEmailVerificationNotification(); } return response()->json([ 'message' => __('api.auth.verification_sent_if_unverified'), ]); } public function forgotPassword(ForgotPasswordRequest $request): JsonResponse { $email = strtolower($request->validated('email')); $status = Password::sendResetLink(['email' => $email]); if ($status === Password::RESET_THROTTLED) { return response()->json([ 'message' => __($status), ], 429); } return response()->json([ 'message' => __('api.auth.password_reset_link_sent'), ]); } public function resetPassword(ResetPasswordRequest $request): JsonResponse { $status = $this->resetPasswordFor($request->safe()); if ($status !== Password::PASSWORD_RESET) { return response()->json([ 'message' => __($status), ], 422); } return response()->json([ 'message' => __('api.auth.password_reset'), ]); } public function showResetPasswordForm(Request $request, string $token): View { $email = (string) $request->query('email', ''); $locale = $this->localeForEmail($email); return view('auth.reset-password', [ 'appName' => config('app.name'), 'email' => $email, 'locale' => $locale, 'passwordReset' => false, 'token' => $token, ]); } public function resetPasswordFromView(ResetPasswordRequest $request): RedirectResponse|View { $locale = $this->localeForEmail((string) $request->validated('email')); app()->setLocale($locale); $status = $this->resetPasswordFor($request->safe()); if ($status !== Password::PASSWORD_RESET) { return back() ->withInput($request->only('email')) ->withErrors(['email' => __($status)]); } return view('auth.reset-password', [ 'appName' => config('app.name'), 'email' => strtolower($request->validated('email')), 'locale' => $locale, 'passwordReset' => true, 'token' => '', ]); } public function logout(): JsonResponse { $request = request(); $accessToken = $request->user()?->currentAccessToken(); if ($accessToken instanceof PersonalAccessToken) { $accessToken->delete(); } if ($request->hasSession()) { Auth::guard('web')->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); } return response()->json([ 'message' => __('api.auth.logout'), ]); } public function me(): JsonResource|JsonResponse { $user = auth()->user(); if (! $user) { return response()->json(['message' => __('api.auth.unauthenticated')], 401); } return new UserResource($user); } public function update(UpdateUserRequest $request): JsonResource { $user = $request->user(); abort_if($user->isSuspended(), 403, __('api.auth.suspended')); $data = $request->validated(); $nutritionGoals = $data['nutritionGoals'] ?? null; if ($request->hasFile('avatar')) { if ($user->avatar_url) { Storage::disk()->delete($user->avatar_url); } $path = $request->file('avatar')->store('avatars'); $data['avatar_url'] = $path; } $userAttributes = $this->userAttributesFromRequestData($data); if (is_array($nutritionGoals)) { $userAttributes['daily_calorie_goal'] = $nutritionGoals['calories']; $userAttributes['daily_protein_goal'] = $nutritionGoals['proteins']; $userAttributes['daily_carbs_goal'] = $nutritionGoals['carbs']; $userAttributes['daily_fats_goal'] = $nutritionGoals['fats']; } if (! empty($userAttributes)) { $user->update($userAttributes); } return new UserResource($user->fresh()); } public function updatePassword(UpdatePasswordRequest $request): JsonResponse { $user = $request->user(); abort_if($user->isSuspended(), 403, __('api.auth.suspended')); $currentAccessToken = $user->currentAccessToken(); $user->forceFill([ 'password' => Hash::make($request->validated('password')), 'remember_token' => Str::random(60), ])->save(); if ($currentAccessToken instanceof PersonalAccessToken) { $user->tokens() ->whereKeyNot($currentAccessToken->getKey()) ->delete(); } else { $user->tokens()->delete(); } return response()->json([ 'message' => __('api.auth.password_updated'), ]); } public function destroy(Request $request): JsonResponse { $user = $request->user(); $storagePaths = collect([$user->avatar_url]) ->merge($user->mealPosts()->withTrashed()->pluck('image_url')) ->filter(fn (?string $path): bool => $this->isPublicStoragePath($path)) ->values() ->all(); DB::transaction(function () use ($user): void { $user->tokens()->delete(); $user->notifications()->delete(); DB::table('sessions') ->where('user_id', $user->getKey()) ->delete(); DB::table('password_reset_tokens') ->where('email', $user->email) ->delete(); $user->forceDelete(); }); if (! empty($storagePaths)) { Storage::disk()->delete($storagePaths); } if ($request->hasSession()) { Auth::guard('web')->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); } return response()->json([ 'message' => __('api.auth.deleted'), ]); } private function createRegisteredUser(RegisterRequest $request): User|JsonResponse { $data = $request->validated(); $avatarPath = null; if ($request->hasFile('avatar')) { $avatarPath = $request->file('avatar')->store('avatars'); } $userAttributes = $this->userAttributesFromRequestData($data); $userAttributes['email'] = strtolower($data['email']); $userAttributes['password'] = Hash::make($data['password']); $userAttributes['avatar_url'] = $avatarPath; $user = User::create($userAttributes); try { $user->sendEmailVerificationNotification(); } catch (Throwable $exception) { report($exception); $user->forceDelete(); if ($avatarPath !== null) { Storage::disk()->delete($avatarPath); } return response()->json([ 'message' => __('api.auth.verification_email_failed'), ], 503); } return $user; } /** * @param array $data */ private function getLoginUser(array $data): User|JsonResponse { $user = User::where('email', strtolower((string) $data['email']))->first(); if (! $user || ! Hash::check((string) $data['password'], $user->password)) { return response()->json([ 'message' => __('api.auth.invalid_credentials'), ], 401); } if ($user->isSuspended()) { return response()->json([ 'message' => __('api.auth.suspended'), ], 403); } if (isset($data['locale']) && $user->locale !== $data['locale']) { $user->forceFill(['locale' => $data['locale']])->save(); } if (! $user->hasVerifiedEmail()) { return response()->json([ 'message' => __('api.auth.email_not_verified'), ], 403); } return $user; } private function startWebSession(Request $request, User $user): void { if (! $request->hasSession()) { return; } Auth::login($user); $request->session()->regenerate(); } private function createMobileToken(User $user, mixed $deviceName): string { $tokenName = is_string($deviceName) && trim($deviceName) !== '' ? trim($deviceName) : 'mobile'; return $user->createToken($tokenName)->plainTextToken; } private function isPublicStoragePath(?string $path): bool { return filled($path) && ! Str::startsWith($path, ['http://', 'https://']); } private function resetPasswordFor(ValidatedInput $data): string { $credentials = $data->all(); $credentials['email'] = strtolower($credentials['email']); return Password::reset( $credentials, function (User $user, string $password): void { $user->forceFill([ 'password' => Hash::make($password), 'remember_token' => Str::random(60), ])->save(); $user->tokens()->delete(); event(new PasswordReset($user)); } ); } private function localeForEmail(string $email): string { return User::where('email', strtolower($email))->value('locale') ?: 'en'; } /** * @param array $data * @return array */ private function userAttributesFromRequestData(array $data): array { $attributes = Arr::only($data, [ 'name', 'avatar_url', 'locale', 'height', 'bio', 'sex', 'date_of_birth', ]); $attributeMap = [ 'physicalActivityLevel' => 'physical_activity_level', 'weightGoal' => 'weight_goal', 'pacePreference' => 'pace_preference', 'dateOfBirth' => 'date_of_birth', ]; foreach ($attributeMap as $requestKey => $attribute) { if (array_key_exists($requestKey, $data)) { $attributes[$attribute] = $data[$requestKey]; } } return $attributes; } }