llp 1 день тому
батько
коміт
250fb9576a

+ 3 - 0
.env.example

@@ -16,6 +16,9 @@ APP_CURRENCY=USD
 APP_MAINTENANCE_DRIVER=file
 # APP_MAINTENANCE_STORE=database
 
+FRONTEND_RESET_PASSWORD_URL_PC=https://pc.sisbridal.com/customer/reset-password
+FRONTEND_RESET_PASSWORD_URL_MOBILE=https://m.sisbridal.com/customer/reset-password
+
 BCRYPT_ROUNDS=12
 
 LOG_CHANNEL=stack

+ 25 - 0
packages/Webkul/BagistoApi/config/bagistoapi.php

@@ -48,6 +48,31 @@ return [
         ],
     ],
 
+    /*
+    |--------------------------------------------------------------------------
+    | Frontend URLs
+    |--------------------------------------------------------------------------
+    |
+    | frontend.reset_password_url
+    |   Generic base URL of the reset-password page on the headless frontend,
+    |   e.g. https://shop.example.com/reset-password. Used when no device
+    |   specific URL below is configured.
+    |
+    | frontend.reset_password_url_pc / frontend.reset_password_url_mobile
+    |   Device specific base URLs. PC requests use reset_password_url_pc,
+    |   mobile requests use reset_password_url_mobile (detected from the
+    |   X-Device-Type header or the User-Agent).
+    |
+    | The token and email are appended as query parameters
+    | (?token=...&email=...). When every option is empty, emails fall back to
+    | the built-in Shop storefront page.
+    */
+    'frontend' => [
+        'reset_password_url'        => env('FRONTEND_RESET_PASSWORD_URL', ''),
+        'reset_password_url_pc'     => env('FRONTEND_RESET_PASSWORD_URL_PC', ''),
+        'reset_password_url_mobile' => env('FRONTEND_RESET_PASSWORD_URL_MOBILE', ''),
+    ],
+
     /*
     |--------------------------------------------------------------------------
     | Payment Reconciliation Job

+ 27 - 0
packages/Webkul/BagistoApi/src/Dto/ResetPasswordInput.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\SerializedName;
+
+class ResetPasswordInput
+{
+    #[ApiProperty(writable: true, readable: false)]
+    #[Groups(['mutation'])]
+    public string $token;
+
+    #[ApiProperty(writable: true, readable: false)]
+    #[Groups(['mutation'])]
+    public string $email;
+
+    #[ApiProperty(writable: true, readable: false)]
+    #[Groups(['mutation'])]
+    public string $password;
+
+    #[ApiProperty(writable: true, readable: false)]
+    #[Groups(['mutation'])]
+    #[SerializedName('passwordConfirmation')]
+    public ?string $passwordConfirmation = null;
+}

+ 69 - 0
packages/Webkul/BagistoApi/src/Models/ResetPassword.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace Webkul\BagistoApi\Models;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GraphQl\Mutation;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\OpenApi\Model\Operation;
+use Webkul\BagistoApi\Dto\ResetPasswordInput;
+use Webkul\BagistoApi\State\ResetPasswordProcessor;
+
+#[ApiResource(
+    routePrefix: '/api/shop',
+    shortName: 'ResetPassword',
+    operations: [
+        new Post(
+            uriTemplate: '/reset-passwords',
+            processor: ResetPasswordProcessor::class,
+            normalizationContext: ['skip_null_values' => false],
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+                'groups'                 => ['mutation'],
+            ],
+            openapi: new Operation(
+                tags: ['Customer'],
+                summary: 'Reset customer password',
+                description: 'Resets the customer password using the token received via the forgot-password email.',
+                requestBody: new \ApiPlatform\OpenApi\Model\RequestBody(
+                    required: true,
+                    content: new \ArrayObject([
+                        'application/json' => [
+                            'schema' => [
+                                'type'     => 'object',
+                                'required' => ['token', 'email', 'password', 'passwordConfirmation'],
+                                'properties' => [
+                                    'token'                => ['type' => 'string'],
+                                    'email'                => ['type' => 'string', 'example' => 'customer@example.com'],
+                                    'password'             => ['type' => 'string'],
+                                    'passwordConfirmation' => ['type' => 'string'],
+                                ],
+                            ],
+                        ],
+                    ]),
+                ),
+            ),
+        ),
+    ],
+    graphQlOperations: [
+        new Mutation(
+            name: 'create',
+            input: ResetPasswordInput::class,
+            output: self::class,
+            processor: ResetPasswordProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+                'groups'                 => ['mutation'],
+            ],
+        ),
+    ]
+)]
+class ResetPassword
+{
+    #[ApiProperty(writable: false, readable: true)]
+    public ?bool $success = null;
+
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $message = null;
+}

+ 12 - 0
packages/Webkul/BagistoApi/src/Resources/lang/en/app.php

@@ -72,6 +72,18 @@ return [
             'error-sending-reset-link'          => 'An error occurred while sending reset link',
         ],
 
+        'reset-password' => [
+            'invalid-operation'                 => 'Invalid operation',
+            'invalid-input-data'                => 'Invalid input data',
+            'token-required'                    => 'Token is required',
+            'email-required'                    => 'Email is required',
+            'password-required'                 => 'New password is required',
+            'password-reset'                    => 'Password has been reset successfully',
+            'invalid-token'                     => 'This password reset token is invalid',
+            'email-not-found'                   => 'Email address not found',
+            'error-resetting'                   => 'An error occurred while resetting password',
+        ],
+
         'logout' => [
             'invalid-operation'                 => 'Invalid operation',
             'invalid-input-data'                => 'Invalid input data',

+ 123 - 0
packages/Webkul/BagistoApi/src/State/ResetPasswordProcessor.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\State\ProcessorInterface;
+use Illuminate\Auth\Events\PasswordReset;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Password;
+use Illuminate\Support\Str;
+use Webkul\BagistoApi\Dto\ResetPasswordInput;
+use Webkul\Customer\Repositories\CustomerRepository;
+
+class ResetPasswordProcessor implements ProcessorInterface
+{
+    public function __construct(protected CustomerRepository $customerRepository) {}
+
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
+    {
+        $defaultResponse = [
+            'success' => false,
+            'message' => '',
+        ];
+
+        $isRestPost = $operation instanceof Post;
+        $isGraphQlCreate = $operation->getName() === 'create';
+
+        if (! $isRestPost && ! $isGraphQlCreate) {
+            $defaultResponse['message'] = __('bagistoapi::app.graphql.reset-password.invalid-operation');
+
+            return (object) $defaultResponse;
+        }
+
+        if ($isRestPost && ! $data instanceof ResetPasswordInput) {
+            $input = new ResetPasswordInput;
+            $input->token = (string) (request()->input('token') ?? '');
+            $input->email = (string) (request()->input('email') ?? '');
+            $input->password = (string) (request()->input('password') ?? '');
+            $input->passwordConfirmation = (string) (request()->input('passwordConfirmation') ?? request()->input('password_confirmation') ?? '');
+            $data = $input;
+        }
+
+        if (! $data instanceof ResetPasswordInput) {
+            $defaultResponse['message'] = __('bagistoapi::app.graphql.reset-password.invalid-input-data');
+
+            return (object) $defaultResponse;
+        }
+
+        if (empty($data->token)) {
+            $defaultResponse['message'] = __('bagistoapi::app.graphql.reset-password.token-required');
+
+            return (object) $defaultResponse;
+        }
+
+        if (empty($data->email)) {
+            $defaultResponse['message'] = __('bagistoapi::app.graphql.reset-password.email-required');
+
+            return (object) $defaultResponse;
+        }
+
+        if (empty($data->password)) {
+            $defaultResponse['message'] = __('bagistoapi::app.graphql.reset-password.password-required');
+
+            return (object) $defaultResponse;
+        }
+
+        try {
+            $response = $this->broker()->reset([
+                'email'                 => $data->email,
+                'password'              => $data->password,
+                'password_confirmation' => $data->passwordConfirmation ?? $data->password,
+                'token'                 => $data->token,
+            ], function ($customer, $password) {
+                $this->resetPassword($customer, $password);
+            });
+
+            if ($response == Password::PASSWORD_RESET) {
+                $customer = $this->customerRepository->findOneByField('email', $data->email);
+
+                if ($customer) {
+                    Event::dispatch('customer.password.update.after', $customer);
+                }
+
+                return (object) [
+                    'success' => true,
+                    'message' => __('bagistoapi::app.graphql.reset-password.password-reset'),
+                ];
+            }
+
+            return (object) [
+                'success' => false,
+                'message' => trans($response),
+            ];
+
+        } catch (\Exception $e) {
+            return (object) [
+                'success' => false,
+                'message' => __('bagistoapi::app.graphql.reset-password.error-resetting'),
+            ];
+        }
+    }
+
+    /**
+     * Reset the given customer password.
+     */
+    protected function resetPassword($customer, $password)
+    {
+        $customer->password = Hash::make($password);
+
+        $customer->setRememberToken(Str::random(60));
+
+        $customer->save();
+
+        event(new PasswordReset($customer));
+    }
+
+    private function broker()
+    {
+        return Password::broker('customers');
+    }
+}

+ 71 - 0
packages/Webkul/BagistoApi/src/Support/Frontend.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace Webkul\BagistoApi\Support;
+
+/**
+ * Resolves headless-frontend URLs.
+ *
+ * Supports separate base URLs per device type (PC / mobile), e.g.
+ * pc.sisbridal.com vs m.sisbridal.com. The device type is taken from the
+ * `X-Device-Type` request header when provided by the frontend, otherwise it
+ * is detected from the User-Agent.
+ */
+class Frontend
+{
+    public const DEVICE_PC = 'pc';
+
+    public const DEVICE_MOBILE = 'mobile';
+
+    /**
+     * Build the reset-password URL for the current request context.
+     *
+     * Resolution order:
+     *   1. `frontend.reset_password_url_mobile`  (mobile requests)
+     *      or `frontend.reset_password_url_pc`   (pc/desktop requests)
+     *   2. `frontend.reset_password_url`         (generic fallback)
+     *   3. built-in Shop storefront reset page   (final fallback)
+     */
+    public static function resetPasswordUrl(string $token, string $email = ''): string
+    {
+        $config = config('bagistoapi.frontend', []);
+
+        $baseUrl = self::isMobileRequest()
+            ? ($config['reset_password_url_mobile'] ?? '')
+            : ($config['reset_password_url_pc'] ?? '');
+
+        if (empty($baseUrl)) {
+            $baseUrl = $config['reset_password_url'] ?? '';
+        }
+
+        if (empty($baseUrl)) {
+            return route('shop.customers.reset_password.create', $token);
+        }
+
+        return rtrim($baseUrl, '/').'?token='.$token.'&email='.urlencode($email);
+    }
+
+    /**
+     * Detect whether the current request comes from a mobile device.
+     *
+     * The frontend may declare the device explicitly via the `X-Device-Type`
+     * header (`mobile` or `pc`); otherwise the User-Agent is inspected.
+     */
+    public static function isMobileRequest(): bool
+    {
+        $request = request();
+
+        $declared = strtolower(trim((string) optional($request)->header('X-Device-Type', '')));
+
+        if (in_array($declared, ['mobile', 'm', 'app'])) {
+            return true;
+        }
+
+        if (in_array($declared, ['pc', 'desktop', 'web'])) {
+            return false;
+        }
+
+        $userAgent = strtolower((string) optional($request)->header('User-Agent', ''));
+
+        return (bool) preg_match('/mobile|android|iphone|ipad|ipod|windows\s?phone|opera\s?mini|iemobile|blackberry/i', $userAgent);
+    }
+}

+ 1 - 0
packages/Webkul/Shop/src/Mail/Customer/ResetPasswordNotification.php

@@ -25,6 +25,7 @@ class ResetPasswordNotification extends ResetPassword
             ->view('shop::emails.customers.forgot-password', [
                 'userName' => $notifiable->name,
                 'token'    => $this->token,
+                'email'    => $notifiable->email,
             ]);
     }
 }

+ 4 - 1
packages/Webkul/Shop/src/Resources/views/emails/customers/forgot-password.blade.php

@@ -14,8 +14,11 @@
     </p>
 
     <div style="display: flex;margin-bottom: 95px">
+        @php
+            $resetPasswordUrl = \Webkul\BagistoApi\Support\Frontend::resetPasswordUrl($token, $email ?? '');
+        @endphp
         <a
-            href="{{ route('shop.customers.reset_password.create', $token) }}"
+            href="{{ $resetPasswordUrl }}"
             style="padding: 16px 45px;justify-content: center;align-items: center;gap: 10px;border-radius: 2px;background: #060C3B;color: #FFFFFF;text-decoration: none;text-transform: uppercase;font-weight: 700;"
         >
             @lang('shop::app.emails.customers.forgot-password.reset-password')