From ae0c21b076ed361512225ea1bc6fb31e2d5966df Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 28 Oct 2025 11:45:05 +0100 Subject: [PATCH 1/3] Initial commit with task details for issue #28 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: undefined --- CLAUDE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index a4a5ebf..ca1981f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -236,3 +236,13 @@ composer test ## Дальнейшее развитие Проект готов к расширению функциональности управления задачами. Текущая база включает полную систему аутентификации и базовые настройки пользователя, что обеспечивает прочную основу для добавления модуля задач. + +--- + +Issue to solve: undefined +Your prepared branch: issue-28-59188d4d +Your prepared working directory: /tmp/gh-issue-solver-1761648298326 +Your forked repository: konard/TaskMateFrontend +Original repository (upstream): xierongchuan/TaskMateFrontend + +Proceed. \ No newline at end of file From 0fa8bef64386092d36974f2330314dcbc239e237 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 28 Oct 2025 11:51:52 +0100 Subject: [PATCH 2/3] feat: transform to frontend-only app with external API integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit completely refactors the application to be a frontend-only Laravel app that connects to the external TaskMate Telegram Bot API. Major changes: Backend Removal: - Removed all local authentication (User model, auth controllers, migrations) - Removed ApiProxyController (no longer needed) - Removed Settings controllers and models - Removed backend authentication middleware and routes - Simplified routes/web.php to only serve views - Removed auth.php routes file Frontend API Client: - Completely rewrote api-client.js to connect directly to external API - API URL now configured via VITE_API_URL environment variable - Authentication uses Bearer tokens stored in localStorage - Automatic token management with 401 handling and redirect Frontend Authentication: - Created auth-guard.js for client-side route protection - Updated login.blade.php with Alpine.js and API integration - Updated register.blade.php with Alpine.js and API integration - Updated header.blade.php for frontend logout and user display - Session management entirely in JavaScript/localStorage Configuration: - Added VITE_API_URL to .env.example - All authentication now handled via external API This transforms the project into a pure frontend application with all business logic handled by the external API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.example | 4 + app/Http/Controllers/ApiProxyController.php | 154 --- .../Auth/ConfirmationController.php | 34 - app/Http/Controllers/Auth/LoginController.php | 79 -- .../Auth/NewPasswordController.php | 54 -- .../Auth/PasswordResetLinkController.php | 28 - .../Auth/RegistrationController.php | 38 - .../Auth/VerificationController.php | 47 - .../Settings/AppearanceController.php | 14 - .../Controllers/Settings/BotApiController.php | 119 --- .../Settings/PasswordController.php | 34 - .../Settings/ProfileController.php | 62 -- app/Models/Setting.php | 94 -- app/Models/User.php | 60 -- database/factories/UserFactory.php | 44 - .../0001_01_01_000000_create_users_table.php | 49 - ...025_10_24_192717_create_settings_table.php | 34 - resources/js/api-client.js | 906 +++++++++--------- resources/js/app.js | 19 +- resources/js/auth-guard.js | 88 ++ resources/js/bootstrap.js | 36 +- .../views/auth/confirm-password.blade.php | 33 - .../views/auth/forgot-password.blade.php | 38 - resources/views/auth/login.blade.php | 125 ++- resources/views/auth/register.blade.php | 126 ++- resources/views/auth/reset-password.blade.php | 46 - resources/views/auth/verify-email.blade.php | 38 - .../components/layouts/app/header.blade.php | 74 +- routes/auth.php | 34 - routes/web.php | 193 ++-- tests/Feature/Auth/AuthenticationTest.php | 41 - tests/Feature/Auth/EmailVerificationTest.php | 47 - .../Feature/Auth/PasswordConfirmationTest.php | 32 - tests/Feature/Auth/PasswordResetTest.php | 60 -- tests/Feature/Auth/RegistrationTest.php | 19 - tests/Feature/Settings/PasswordUpdateTest.php | 40 - tests/Feature/Settings/ProfileUpdateTest.php | 62 -- 37 files changed, 981 insertions(+), 2024 deletions(-) delete mode 100644 app/Http/Controllers/ApiProxyController.php delete mode 100644 app/Http/Controllers/Auth/ConfirmationController.php delete mode 100644 app/Http/Controllers/Auth/LoginController.php delete mode 100644 app/Http/Controllers/Auth/NewPasswordController.php delete mode 100644 app/Http/Controllers/Auth/PasswordResetLinkController.php delete mode 100644 app/Http/Controllers/Auth/RegistrationController.php delete mode 100644 app/Http/Controllers/Auth/VerificationController.php delete mode 100644 app/Http/Controllers/Settings/AppearanceController.php delete mode 100644 app/Http/Controllers/Settings/BotApiController.php delete mode 100644 app/Http/Controllers/Settings/PasswordController.php delete mode 100644 app/Http/Controllers/Settings/ProfileController.php delete mode 100644 app/Models/Setting.php delete mode 100644 app/Models/User.php delete mode 100644 database/factories/UserFactory.php delete mode 100644 database/migrations/0001_01_01_000000_create_users_table.php delete mode 100644 database/migrations/2025_10_24_192717_create_settings_table.php create mode 100644 resources/js/auth-guard.js delete mode 100644 resources/views/auth/confirm-password.blade.php delete mode 100644 resources/views/auth/forgot-password.blade.php delete mode 100644 resources/views/auth/reset-password.blade.php delete mode 100644 resources/views/auth/verify-email.blade.php delete mode 100644 routes/auth.php delete mode 100644 tests/Feature/Auth/AuthenticationTest.php delete mode 100644 tests/Feature/Auth/EmailVerificationTest.php delete mode 100644 tests/Feature/Auth/PasswordConfirmationTest.php delete mode 100644 tests/Feature/Auth/PasswordResetTest.php delete mode 100644 tests/Feature/Auth/RegistrationTest.php delete mode 100644 tests/Feature/Settings/PasswordUpdateTest.php delete mode 100644 tests/Feature/Settings/ProfileUpdateTest.php diff --git a/.env.example b/.env.example index ca01994..b7c4e8d 100644 --- a/.env.example +++ b/.env.example @@ -63,3 +63,7 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +# TaskMate Telegram Bot API Configuration +# URL of the external API (without /api/v1 suffix) +VITE_API_URL=http://localhost:8007/api/v1 diff --git a/app/Http/Controllers/ApiProxyController.php b/app/Http/Controllers/ApiProxyController.php deleted file mode 100644 index 49fc055..0000000 --- a/app/Http/Controllers/ApiProxyController.php +++ /dev/null @@ -1,154 +0,0 @@ -json([ - 'error' => 'API token not configured', - 'message' => 'Please configure your Telegram Bot API token in settings' - ], 401); - } - - // Replace localhost with host.docker.internal for Docker environment - $apiUrl = str_replace('http://localhost:', 'http://host.docker.internal:', $apiUrl); - - // Build the external URL - $externalUrl = rtrim($apiUrl, '/') . '/' . ltrim($endpoint, '/'); - - // Prepare request headers - $headers = [ - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - 'Authorization' => "Bearer {$apiToken}", - ]; - - // Add any custom headers from the original request - if ($request->header('X-Requested-With')) { - $headers['X-Requested-With'] = $request->header('X-Requested-With'); - } - - // Make the external request - $response = Http::withHeaders($headers) - ->timeout(30) - ->send( - $request->method(), - $externalUrl, - [ - 'json' => $request->getContent() ? $request->json()->all() : [], - 'query' => $request->query->all(), - ] - ); - - // Return the response with the same status code - return response()->json( - $response->json(), - $response->status() - ); - - } catch (\Exception $e) { - // Log the error for debugging - \Log::error('API Proxy Error: ' . $e->getMessage(), [ - 'endpoint' => $endpoint, - 'method' => $request->method(), - 'user_id' => auth()->id(), - ]); - - return response()->json([ - 'error' => 'Proxy request failed', - 'message' => 'Unable to connect to external API', - 'details' => config('app.debug') ? $e->getMessage() : null - ], 500); - } - } - - /** - * Handle file uploads through proxy - */ - public function proxyUpload(Request $request, string $endpoint): JsonResponse - { - try { - // Get user's API settings - $apiUrl = Setting::getValue('api_url', 'http://host.docker.internal:8007/api/v1'); - $apiToken = Setting::getValue('auth_token'); - - if (!$apiToken) { - return response()->json([ - 'error' => 'API token not configured' - ], 401); - } - - // Replace localhost with host.docker.internal for Docker environment - $apiUrl = str_replace('http://localhost:', 'http://host.docker.internal:', $apiUrl); - - $externalUrl = rtrim($apiUrl, '/') . '/' . ltrim($endpoint, '/'); - - // Prepare headers - $headers = [ - 'Accept' => 'application/json', - 'Authorization' => "Bearer {$apiToken}", - ]; - - // Handle file uploads - $files = []; - foreach ($request->allFiles() as $key => $file) { - $files[$key] = fopen($file->getPathname(), 'r'); - } - - // Prepare form data - $data = $request->except(array_keys($request->allFiles())); - - // Make the request with files - $response = Http::asMultipart() - ->withHeaders($headers) - ->timeout(60) - ->attach( - array_map(fn($file, $key) => [ - 'name' => $key, - 'contents' => $file, - 'filename' => $request->file($key)->getClientOriginalName() - ], $files, array_keys($files)) - ) - ->post($externalUrl, $data); - - // Close file handles - foreach ($files as $file) { - if (is_resource($file)) { - fclose($file); - } - } - - return response()->json( - $response->json(), - $response->status() - ); - - } catch (\Exception $e) { - \Log::error('API Proxy Upload Error: ' . $e->getMessage(), [ - 'endpoint' => $endpoint, - 'user_id' => auth()->id(), - ]); - - return response()->json([ - 'error' => 'Upload failed', - 'message' => 'Unable to upload file to external API' - ], 500); - } - } -} diff --git a/app/Http/Controllers/Auth/ConfirmationController.php b/app/Http/Controllers/Auth/ConfirmationController.php deleted file mode 100644 index 0a54e65..0000000 --- a/app/Http/Controllers/Auth/ConfirmationController.php +++ /dev/null @@ -1,34 +0,0 @@ -validate([ - 'email' => $request->user()->email, - 'password' => $request->password, - ])) { - throw ValidationException::withMessages([ - 'password' => __('auth.password'), - ]); - } - - $request->session()->put('auth.password_confirmed_at', time()); - - return redirect()->intended(route('dashboard', absolute: false)); - } -} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php deleted file mode 100644 index 46ab9e5..0000000 --- a/app/Http/Controllers/Auth/LoginController.php +++ /dev/null @@ -1,79 +0,0 @@ -validate([ - 'email' => ['required', 'string', 'email'], - 'password' => ['required', 'string'], - ]); - - $this->ensureIsNotRateLimited($request); - - if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) { - RateLimiter::hit($this->throttleKey($request)); - - throw ValidationException::withMessages([ - 'email' => trans('auth.failed'), - ]); - } - - RateLimiter::clear($this->throttleKey($request)); - - $request->session()->regenerate(); - - return redirect()->intended(route('dashboard', absolute: false)); - } - - public function destroy(Request $request): RedirectResponse - { - Auth::guard('web')->logout(); - - $request->session()->invalidate(); - - $request->session()->regenerateToken(); - - return redirect('/'); - } - - protected function ensureIsNotRateLimited(Request $request): void - { - if (! RateLimiter::tooManyAttempts($this->throttleKey($request), 5)) { - return; - } - - event(new Lockout($request)); - - $seconds = RateLimiter::availableIn($this->throttleKey($request)); - - throw ValidationException::withMessages([ - 'email' => trans('auth.throttle', [ - 'seconds' => $seconds, - 'minutes' => ceil($seconds / 60), - ]), - ]); - } - - public function throttleKey(Request $request): string - { - return Str::transliterate(Str::lower($request->string('email')).'|'.$request->ip()); - } -} diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php deleted file mode 100644 index 5178ab0..0000000 --- a/app/Http/Controllers/Auth/NewPasswordController.php +++ /dev/null @@ -1,54 +0,0 @@ - $request]); - } - - public function store(Request $request): RedirectResponse - { - $request->validate([ - 'token' => ['required'], - 'email' => ['required', 'email'], - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - // Here we will attempt to reset the user's password. If it is successful we - // will update the password on an actual user model and persist it to the - // database. Otherwise we will parse the error and return the response. - $status = Password::reset( - $request->only('email', 'password', 'password_confirmation', 'token'), - function (User $user) use ($request) { - $user->forceFill([ - 'password' => Hash::make($request->password), - 'remember_token' => Str::random(60), - ])->save(); - - event(new PasswordReset($user)); - } - ); - - // If the password was successfully reset, we will redirect the user back to - // the application's home authenticated view. If there is an error we can - // redirect them back to where they came from with their error message. - return $status == Password::PASSWORD_RESET - ? to_route('login')->with('status', __($status)) - : back()->withInput($request->only('email')) - ->withErrors(['email' => __($status)]); - } -} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php deleted file mode 100644 index 6ea25e2..0000000 --- a/app/Http/Controllers/Auth/PasswordResetLinkController.php +++ /dev/null @@ -1,28 +0,0 @@ -validate([ - 'email' => ['required', 'email'], - ]); - - Password::sendResetLink($request->only('email')); - - return back()->with('status', __('A reset link will be sent if the account exists.')); - } -} diff --git a/app/Http/Controllers/Auth/RegistrationController.php b/app/Http/Controllers/Auth/RegistrationController.php deleted file mode 100644 index 4c33bec..0000000 --- a/app/Http/Controllers/Auth/RegistrationController.php +++ /dev/null @@ -1,38 +0,0 @@ -validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - $validated['password'] = Hash::make($validated['password']); - - event(new Registered(($user = User::create($validated)))); - - Auth::login($user); - - return redirect(route('dashboard', absolute: false)); - } -} diff --git a/app/Http/Controllers/Auth/VerificationController.php b/app/Http/Controllers/Auth/VerificationController.php deleted file mode 100644 index ed60bf4..0000000 --- a/app/Http/Controllers/Auth/VerificationController.php +++ /dev/null @@ -1,47 +0,0 @@ -user()->hasVerifiedEmail() - ? redirect()->intended(route('dashboard', absolute: false)) - : view('auth.verify-email'); - } - - public function store(Request $request): RedirectResponse - { - if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false)); - } - - $request->user()->sendEmailVerificationNotification(); - - return back()->with('status', 'verification-link-sent'); - } - - public function verify(EmailVerificationRequest $request): RedirectResponse - { - if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); - } - - if ($request->user()->markEmailAsVerified()) { - /** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */ - $user = $request->user(); - - event(new Verified($user)); - } - - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); - } -} diff --git a/app/Http/Controllers/Settings/AppearanceController.php b/app/Http/Controllers/Settings/AppearanceController.php deleted file mode 100644 index f8caff7..0000000 --- a/app/Http/Controllers/Settings/AppearanceController.php +++ /dev/null @@ -1,14 +0,0 @@ -get(); - - return response()->json([ - 'data' => $settings - ]); - } - - /** - * Get specific setting by key. - */ - public function show(string $key): JsonResponse - { - $setting = Setting::where('user_id', Auth::id()) - ->where('key', $key) - ->first(); - - if (!$setting) { - return response()->json(['message' => 'Setting not found'], 404); - } - - return response()->json(['data' => $setting]); - } - - /** - * Update or create a setting. - */ - public function update(Request $request, string $key): JsonResponse - { - $validator = Validator::make($request->all(), [ - 'value' => 'required|string', - 'type' => 'sometimes|string|in:string,integer,float,boolean,json' - ]); - - if ($validator->fails()) { - return response()->json(['errors' => $validator->errors()], 422); - } - - $setting = Setting::setValue( - $key, - $request->input('value'), - $request->input('type', 'string') - ); - - return response()->json(['data' => $setting]); - } - - /** - * Bulk update settings. - */ - public function bulkUpdate(Request $request): JsonResponse - { - $validator = Validator::make($request->all(), [ - 'settings' => 'required|array', - 'settings.*.key' => 'required|string', - 'settings.*.value' => 'required', - 'settings.*.type' => 'sometimes|string|in:string,integer,float,boolean,json' - ]); - - if ($validator->fails()) { - return response()->json(['errors' => $validator->errors()], 422); - } - - $settingsData = []; - foreach ($request->input('settings') as $setting) { - $settingsData[$setting['key']] = [ - 'value' => $setting['value'], - 'type' => $setting['type'] ?? 'string' - ]; - } - - $results = Setting::setBulk($settingsData); - - return response()->json(['data' => array_values($results)]); - } - - /** - * Delete a setting. - */ - public function destroy(string $key): JsonResponse - { - $setting = Setting::where('user_id', Auth::id()) - ->where('key', $key) - ->first(); - - if (!$setting) { - return response()->json(['message' => 'Setting not found'], 404); - } - - $setting->delete(); - - return response()->json(['message' => 'Setting deleted successfully']); - } -} diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php deleted file mode 100644 index 60d3685..0000000 --- a/app/Http/Controllers/Settings/PasswordController.php +++ /dev/null @@ -1,34 +0,0 @@ - $request->user(), - ]); - } - - public function update(Request $request): RedirectResponse - { - $validated = $request->validate([ - 'current_password' => ['required', 'current_password'], - 'password' => ['required', Rules\Password::defaults(), 'confirmed'], - ]); - - $request->user()->update([ - 'password' => Hash::make($validated['password']), - ]); - - return back()->with('status', 'password-updated'); - } -} diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php deleted file mode 100644 index a0c1bc9..0000000 --- a/app/Http/Controllers/Settings/ProfileController.php +++ /dev/null @@ -1,62 +0,0 @@ - $request->user(), - ]); - } - - public function update(Request $request): RedirectResponse - { - $user = $request->user(); - - $validated = $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => [ - 'required', - 'string', - 'lowercase', - 'email', - 'max:255', - Rule::unique(User::class)->ignore($user->id), - ], - ]); - - $user->fill($validated); - - if ($user->isDirty('email')) { - $user->email_verified_at = null; - } - - $user->save(); - - return to_route('settings.profile.edit')->with('status', __('Profile updated successfully')); - } - - public function destroy(Request $request): RedirectResponse - { - $user = $request->user(); - - Auth::logout(); - - $user->delete(); - - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return to_route('home'); - } -} diff --git a/app/Models/Setting.php b/app/Models/Setting.php deleted file mode 100644 index 711952b..0000000 --- a/app/Models/Setting.php +++ /dev/null @@ -1,94 +0,0 @@ - 'string', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - ]; - - public function user() - { - return $this->belongsTo(User::class); - } - - public static function getValue(string $key, mixed $default = null, ?int $userId = null) - { - $userId = $userId ?? auth()->id(); - - $setting = static::where('user_id', $userId) - ->where('key', $key) - ->first(); - - if (!$setting) { - return $default; - } - - return match ($setting->type) { - 'boolean' => (bool) $setting->value, - 'integer' => (int) $setting->value, - 'float' => (float) $setting->value, - 'json' => json_decode($setting->value, true), - default => $setting->value, - }; - } - - public static function setValue(string $key, mixed $value, string $type = 'string', ?int $userId = null) - { - $userId = $userId ?? auth()->id(); - - $processedValue = match ($type) { - 'boolean' => $value ? 'true' : 'false', - 'json' => json_encode($value), - default => (string) $value, - }; - - return static::updateOrCreate( - ['user_id' => $userId, 'key' => $key], - ['value' => $processedValue, 'type' => $type] - ); - } - - public static function getBulk(array $keys, ?int $userId = null) - { - $userId = $userId ?? auth()->id(); - - return static::where('user_id', $userId) - ->whereIn('key', $keys) - ->get() - ->mapWithKeys(function ($setting) { - return [$setting->key => static::getValue($setting->key, null, $setting->user_id)]; - }) - ->toArray(); - } - - public static function setBulk(array $settings, ?int $userId = null) - { - $userId = $userId ?? auth()->id(); - $results = []; - - foreach ($settings as $key => $data) { - $value = $data['value'] ?? null; - $type = $data['type'] ?? 'string'; - - $results[$key] = static::setValue($key, $value, $type, $userId); - } - - return $results; - } -} diff --git a/app/Models/User.php b/app/Models/User.php deleted file mode 100644 index 2d9bad8..0000000 --- a/app/Models/User.php +++ /dev/null @@ -1,60 +0,0 @@ - */ - use HasFactory, Notifiable; - - /** - * The attributes that are mass assignable. - * - * @var list - */ - protected $fillable = [ - 'name', - 'email', - 'password', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } - - /** - * Get the user's initials - */ - public function initials(): string - { - return Str::of($this->name) - ->explode(' ') - ->map(fn (string $name) => Str::of($name)->substr(0, 1)) - ->implode(''); - } -} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php deleted file mode 100644 index 584104c..0000000 --- a/database/factories/UserFactory.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class UserFactory extends Factory -{ - /** - * The current password being used by the factory. - */ - protected static ?string $password; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), - ]; - } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } -} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php deleted file mode 100644 index 05fb5d9..0000000 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/database/migrations/2025_10_24_192717_create_settings_table.php b/database/migrations/2025_10_24_192717_create_settings_table.php deleted file mode 100644 index 7f93038..0000000 --- a/database/migrations/2025_10_24_192717_create_settings_table.php +++ /dev/null @@ -1,34 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->onDelete('cascade'); - $table->string('key'); - $table->text('value')->nullable(); - $table->string('type')->default('string'); - $table->timestamps(); - - $table->unique(['user_id', 'key']); - $table->index(['user_id', 'key']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('settings'); - } -}; diff --git a/resources/js/api-client.js b/resources/js/api-client.js index 41c7899..11a93b2 100644 --- a/resources/js/api-client.js +++ b/resources/js/api-client.js @@ -2,446 +2,480 @@ * API Client for TaskMate Telegram Bot API * * This module provides a client for communicating with the TaskMate Telegram Bot API. - * All data is fetched via AJAX requests through a local proxy that forwards requests - * to the external API configured in user settings. + * All data is fetched via direct AJAX requests to the external API configured via environment variables. + * Authentication is handled via Bearer tokens stored in localStorage. */ class ApiClient { - constructor() { - // API base URL - now uses local proxy endpoint - this.baseUrl = window.API_BASE_URL || '/api/proxy'; - // Token is now handled server-side in the proxy - this.token = null; - } - - /** - * Refresh CSRF token - */ - async refreshCsrfToken() { - try { - const response = await fetch('/csrf-token', { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - credentials: 'same-origin' - }); - - if (response.ok) { - const data = await response.json(); - if (data.csrf_token) { - // Update meta tag - const metaTag = document.querySelector('meta[name="csrf-token"]'); - if (metaTag) { - metaTag.setAttribute('content', data.csrf_token); - } - return data.csrf_token; - } - } - } catch (error) { - console.error('Failed to refresh CSRF token:', error); + constructor() { + // API base URL from environment variables + this.baseUrl = + import.meta.env.VITE_API_URL || "http://localhost:8007/api/v1"; + // Token storage key + this.tokenKey = "taskmate_auth_token"; + this.userKey = "taskmate_user"; + } + + /** + * Get current auth token from localStorage + */ + getToken() { + return localStorage.getItem(this.tokenKey); + } + + /** + * Set auth token in localStorage + */ + setToken(token) { + if (token) { + localStorage.setItem(this.tokenKey, token); + } else { + localStorage.removeItem(this.tokenKey); + } + } + + /** + * Get current user from localStorage + */ + getUser() { + const user = localStorage.getItem(this.userKey); + return user ? JSON.parse(user) : null; + } + + /** + * Set current user in localStorage + */ + setUser(user) { + if (user) { + localStorage.setItem(this.userKey, JSON.stringify(user)); + } else { + localStorage.removeItem(this.userKey); + } + } + + /** + * Clear authentication data + */ + clearAuth() { + this.setToken(null); + this.setUser(null); + } + + /** + * Check if user is authenticated + */ + isAuthenticated() { + return !!this.getToken(); + } + + /** + * Make an API request + */ + async request(endpoint, options = {}) { + const url = `${this.baseUrl}${endpoint}`; + const headers = { + "Content-Type": "application/json", + Accept: "application/json", + ...options.headers, + }; + + // Add Authorization header if token exists + const token = this.getToken(); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + const config = { + ...options, + headers, + }; + + try { + const response = await fetch(url, config); + + // Handle 401 Unauthorized - clear auth and redirect to login + if (response.status === 401) { + this.clearAuth(); + if (window.location.pathname !== "/login") { + window.location.href = "/login?expired=1"; } - return null; - } + throw new Error("Authentication required. Please login again."); + } - /** - * Get current CSRF token - */ - getCsrfToken() { - return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); - } - - /** - * Make an API request - */ - async request(endpoint, options = {}) { - const url = `${this.baseUrl}${endpoint}`; - const headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - ...options.headers, - }; - - // Add CSRF token for web routes - const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); - if (csrfToken) { - headers['X-CSRF-TOKEN'] = csrfToken; - } + const data = await response.json(); - // Token is now handled server-side in the proxy - // No need to send Authorization header from frontend - - const config = { - ...options, - headers, - credentials: 'same-origin', // Important for CSRF cookies - }; - - try { - const response = await fetch(url, config); - const data = await response.json(); - - if (!response.ok) { - // Handle CSRF errors - if (response.status === 419) { - throw new Error('CSRF token expired. Please refresh the page and try again.'); - } - - // Handle validation errors (including CSRF) - if (response.status === 422 && data.errors) { - const errorMessage = data.errors?.['X-CSRF-TOKEN']?.[0] || - data.errors?.['csrf_token']?.[0] || - Object.values(data.errors).flat().join(', ') || - data.message; - throw new Error(errorMessage || 'Validation failed'); - } - - throw new Error(data.message || 'API request failed'); - } - - return data; - } catch (error) { - console.error('API request error:', error); - - // If CSRF token is missing or expired, try to refresh it - if (error.message.includes('CSRF') || error.message.includes('419')) { - console.warn('CSRF token issue detected, attempting to refresh...'); - - // Try to refresh the token and retry the request once - try { - const newToken = await this.refreshCsrfToken(); - if (newToken) { - console.log('CSRF token refreshed, retrying request...'); - // Update headers with new token - config.headers['X-CSRF-TOKEN'] = newToken; - - // Retry the request - const retryResponse = await fetch(url, config); - const retryData = await retryResponse.json(); - - if (retryResponse.ok) { - return retryData; - } else { - throw new Error(retryData.message || 'API request failed after token refresh'); - } - } - } catch (retryError) { - console.error('Failed to retry request after CSRF refresh:', retryError); - error.message = 'CSRF validation failed. Please refresh the page and try again.'; - } - } - - throw error; + if (!response.ok) { + // Handle validation errors + if (response.status === 422 && data.errors) { + const errorMessage = + Object.values(data.errors).flat().join(", ") || data.message; + throw new Error(errorMessage || "Validation failed"); } - } - - /** - * GET request - */ - async get(endpoint, params = {}) { - const query = new URLSearchParams(params).toString(); - const url = query ? `${endpoint}?${query}` : endpoint; - return this.request(url, { method: 'GET' }); - } - - /** - * POST request - */ - async post(endpoint, data = {}) { - return this.request(endpoint, { - method: 'POST', - body: JSON.stringify(data), - }); - } - - /** - * PUT request - */ - async put(endpoint, data = {}) { - return this.request(endpoint, { - method: 'PUT', - body: JSON.stringify(data), - }); - } - - /** - * DELETE request - */ - async delete(endpoint) { - return this.request(endpoint, { method: 'DELETE' }); - } - - // ============================================ - // Authentication Endpoints - // ============================================ - - /** - * Login user - */ - async login(login, password) { - const data = await this.post('/session', { login, password }); - // Token is now handled server-side - return data; - } - - /** - * Logout user - */ - async logout() { - await this.delete('/session'); - // Token is now handled server-side - } - - /** - * Register new user - */ - async register(login, password) { - const data = await this.post('/register', { login, password }); - // Token is now handled server-side - return data; - } - - /** - * Health check - */ - async healthCheck() { - return this.get('/up'); - } - - // ============================================ - // Users Endpoints - // ============================================ - - /** - * Get list of users - */ - async getUsers(params = {}) { - return this.get('/users', params); - } - - /** - * Get user by ID - */ - async getUser(id) { - return this.get(`/users/${id}`); - } - - /** - * Get user status - */ - async getUserStatus(id) { - return this.get(`/users/${id}/status`); - } - - /** - * Create new user (public endpoint) - * Uses the public endpoint for creating users without authentication - */ - async createUser(data) { - return this.post('/users/create', data); - } - - /** - * Update user - * Note: This endpoint may not be available in the Telegram Bot API - * as users are managed through the Telegram bot registration process - */ - async updateUser(id, data) { - return this.put(`/users/${id}`, data); - } - - /** - * Delete user - */ - async deleteUser(id) { - return this.delete(`/users/${id}`); - } - - // ============================================ - // Dealerships Endpoints - // ============================================ - - /** - * Get list of dealerships - */ - async getDealerships(params = {}) { - return this.get('/dealerships', params); - } - - /** - * Get dealership by ID - */ - async getDealership(id) { - return this.get(`/dealerships/${id}`); - } - - /** - * Create new dealership - */ - async createDealership(data) { - return this.post('/dealerships', data); - } - - /** - * Update dealership - */ - async updateDealership(id, data) { - return this.put(`/dealerships/${id}`, data); - } - /** - * Delete dealership - */ - async deleteDealership(id) { - return this.delete(`/dealerships/${id}`); - } - - // ============================================ - // Shifts Endpoints - // ============================================ - - /** - * Get list of shifts - */ - async getShifts(params = {}) { - return this.get('/shifts', params); - } - - /** - * Get shift by ID - */ - async getShift(id) { - return this.get(`/shifts/${id}`); - } - - /** - * Get current open shifts - */ - async getCurrentShifts(dealershipId = null) { - const params = dealershipId ? { dealership_id: dealershipId } : {}; - return this.get('/shifts/current', params); - } - - /** - * Get shift statistics - */ - async getShiftStatistics(params = {}) { - return this.get('/shifts/statistics', params); - } - - // ============================================ - // Tasks Endpoints - // ============================================ - - /** - * Get list of tasks - */ - async getTasks(params = {}) { - return this.get('/tasks', params); - } - - /** - * Get task by ID - */ - async getTask(id) { - return this.get(`/tasks/${id}`); - } - - /** - * Create new task - */ - async createTask(data) { - return this.post('/tasks', data); - } - - /** - * Update task - */ - async updateTask(id, data) { - return this.put(`/tasks/${id}`, data); - } - - /** - * Delete task - */ - async deleteTask(id) { - return this.delete(`/tasks/${id}`); - } - - /** - * Get task statistics - */ - async getTaskStatistics(params = {}) { - return this.get('/tasks/statistics', params); - } - - /** - * Get overdue tasks - */ - async getOverdueTasks(params = {}) { - return this.get('/tasks/overdue', params); - } - - /** - * Get postponed tasks - */ - async getPostponedTasks(params = {}) { - return this.get('/tasks/postponed', params); - } - - // ============================================ - // Task Responses Endpoints - // ============================================ - - /** - * Update task response - */ - async updateTaskResponse(taskId, data) { - return this.put(`/tasks/${taskId}/responses`, data); - } - - // ============================================ - // Dashboard Endpoints - // ============================================ - - /** - * Get dashboard data - */ - async getDashboard(params = {}) { - return this.get('/dashboard', params); - } - - // ============================================ - // Settings Endpoints - // ============================================ - - /** - * Get settings - */ - async getSettings(params = {}) { - return this.get('/settings', params); - } - - /** - * Get setting by key - */ - async getSetting(key) { - return this.get(`/settings/${key}`); - } - - /** - * Update setting - */ - async updateSetting(key, value) { - return this.put(`/settings/${key}`, { value }); - } - - /** - * Bulk update settings - */ - async bulkUpdateSettings(settings) { - return this.post('/settings/bulk', { settings }); - } + throw new Error( + data.message || `API request failed with status ${response.status}`, + ); + } + + return data; + } catch (error) { + console.error("API request error:", error); + throw error; + } + } + + /** + * GET request + */ + async get(endpoint, params = {}) { + const query = new URLSearchParams(params).toString(); + const url = query ? `${endpoint}?${query}` : endpoint; + return this.request(url, { method: "GET" }); + } + + /** + * POST request + */ + async post(endpoint, data = {}) { + return this.request(endpoint, { + method: "POST", + body: JSON.stringify(data), + }); + } + + /** + * PUT request + */ + async put(endpoint, data = {}) { + return this.request(endpoint, { + method: "PUT", + body: JSON.stringify(data), + }); + } + + /** + * DELETE request + */ + async delete(endpoint) { + return this.request(endpoint, { method: "DELETE" }); + } + + // ============================================ + // Authentication Endpoints + // ============================================ + + /** + * Login user + * @param {string} login - Username or phone + * @param {string} password - User password + * @returns {Promise<{token: string, user: object}>} + */ + async login(login, password) { + const data = await this.post("/session", { login, password }); + + // Store token and user data + if (data.token) { + this.setToken(data.token); + } + if (data.user) { + this.setUser(data.user); + } + + return data; + } + + /** + * Logout user + */ + async logout() { + try { + // Call API logout endpoint if authenticated + if (this.isAuthenticated()) { + await this.delete("/session"); + } + } catch (error) { + console.error("Logout API call failed:", error); + // Continue with local logout even if API call fails + } finally { + // Always clear local auth data + this.clearAuth(); + } + } + + /** + * Register new user + * @param {string} login - Username or phone + * @param {string} password - User password + * @returns {Promise<{token: string, user: object}>} + */ + async register(login, password) { + const data = await this.post("/register", { login, password }); + + // Store token and user data if registration returns them + if (data.token) { + this.setToken(data.token); + } + if (data.user) { + this.setUser(data.user); + } + + return data; + } + + /** + * Health check + */ + async healthCheck() { + return this.get("/up"); + } + + /** + * Get current user data from API + */ + async getCurrentUser() { + const user = this.getUser(); + if (!user || !user.id) { + throw new Error("No user data available"); + } + + // Fetch fresh user data from API + const data = await this.getUser(user.id); + + // Update stored user data + if (data.user || data.data) { + this.setUser(data.user || data.data); + } + + return data; + } + + // ============================================ + // Users Endpoints + // ============================================ + + /** + * Get list of users + */ + async getUsers(params = {}) { + return this.get("/users", params); + } + + /** + * Get user by ID + */ + async getUser(id) { + return this.get(`/users/${id}`); + } + + /** + * Get user status + */ + async getUserStatus(id) { + return this.get(`/users/${id}/status`); + } + + /** + * Create new user (public endpoint) + */ + async createUser(data) { + return this.post("/users/create", data); + } + + /** + * Update user + */ + async updateUser(id, data) { + return this.put(`/users/${id}`, data); + } + + /** + * Delete user + */ + async deleteUser(id) { + return this.delete(`/users/${id}`); + } + + // ============================================ + // Dealerships Endpoints + // ============================================ + + /** + * Get list of dealerships + */ + async getDealerships(params = {}) { + return this.get("/dealerships", params); + } + + /** + * Get dealership by ID + */ + async getDealership(id) { + return this.get(`/dealerships/${id}`); + } + + /** + * Create new dealership + */ + async createDealership(data) { + return this.post("/dealerships", data); + } + + /** + * Update dealership + */ + async updateDealership(id, data) { + return this.put(`/dealerships/${id}`, data); + } + + /** + * Delete dealership + */ + async deleteDealership(id) { + return this.delete(`/dealerships/${id}`); + } + + // ============================================ + // Shifts Endpoints + // ============================================ + + /** + * Get list of shifts + */ + async getShifts(params = {}) { + return this.get("/shifts", params); + } + + /** + * Get shift by ID + */ + async getShift(id) { + return this.get(`/shifts/${id}`); + } + + /** + * Get current open shifts + */ + async getCurrentShifts(dealershipId = null) { + const params = dealershipId ? { dealership_id: dealershipId } : {}; + return this.get("/shifts/current", params); + } + + /** + * Get shift statistics + */ + async getShiftStatistics(params = {}) { + return this.get("/shifts/statistics", params); + } + + // ============================================ + // Tasks Endpoints + // ============================================ + + /** + * Get list of tasks + */ + async getTasks(params = {}) { + return this.get("/tasks", params); + } + + /** + * Get task by ID + */ + async getTask(id) { + return this.get(`/tasks/${id}`); + } + + /** + * Create new task + */ + async createTask(data) { + return this.post("/tasks", data); + } + + /** + * Update task + */ + async updateTask(id, data) { + return this.put(`/tasks/${id}`, data); + } + + /** + * Delete task + */ + async deleteTask(id) { + return this.delete(`/tasks/${id}`); + } + + /** + * Get postponed tasks + */ + async getPostponedTasks(params = {}) { + return this.get("/tasks/postponed", params); + } + + // ============================================ + // Dashboard Endpoints + // ============================================ + + /** + * Get dashboard data + */ + async getDashboard(params = {}) { + return this.get("/dashboard", params); + } + + // ============================================ + // Settings Endpoints + // ============================================ + + /** + * Get settings + */ + async getSettings(params = {}) { + return this.get("/settings", params); + } + + /** + * Get setting by key + */ + async getSetting(key) { + return this.get(`/settings/${key}`); + } + + /** + * Create or update settings + */ + async createOrUpdateSettings(data) { + return this.post("/settings", data); + } + + /** + * Update setting by ID + */ + async updateSetting(id, data) { + return this.put(`/settings/${id}`, data); + } + + /** + * Delete setting + */ + async deleteSetting(id) { + return this.delete(`/settings/${id}`); + } + + /** + * Get shift configuration settings + */ + async getShiftConfig() { + return this.get("/settings/shift-config"); + } + + /** + * Update shift configuration settings + */ + async updateShiftConfig(data) { + return this.post("/settings/shift-config", data); + } } // Create and export a singleton instance @@ -451,4 +485,12 @@ window.apiClient = apiClient; // Mark API client as ready window.apiClientReady = true; +// Expose for debugging +if (import.meta.env.DEV) { + console.log("API Client initialized", { + baseUrl: apiClient.baseUrl, + isAuthenticated: apiClient.isAuthenticated(), + }); +} + export default apiClient; diff --git a/resources/js/app.js b/resources/js/app.js index cc31c2d..bf9816c 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,14 +1,15 @@ import "./bootstrap"; import "./api-client"; +import "./auth-guard"; // Ensure Alpine.js initializes after API client -document.addEventListener('DOMContentLoaded', () => { - if (!window.apiClientReady) { - console.warn('API client not ready when DOM loaded, waiting...'); - setTimeout(() => { - if (!window.apiClientReady) { - console.error('API client still not ready after delay'); - } - }, 500); - } +document.addEventListener("DOMContentLoaded", () => { + if (!window.apiClientReady) { + console.warn("API client not ready when DOM loaded, waiting..."); + setTimeout(() => { + if (!window.apiClientReady) { + console.error("API client still not ready after delay"); + } + }, 500); + } }); diff --git a/resources/js/auth-guard.js b/resources/js/auth-guard.js new file mode 100644 index 0000000..a8c2a42 --- /dev/null +++ b/resources/js/auth-guard.js @@ -0,0 +1,88 @@ +/** + * Frontend Authentication Guard + * + * This module provides client-side route protection by checking authentication status + * and redirecting unauthenticated users to login page. + */ + +import apiClient from "./api-client"; + +/** + * Check if current route requires authentication + */ +function requiresAuth() { + const publicPaths = [ + "/", + "/login", + "/register", + "/forgot-password", + "/reset-password", + ]; + const currentPath = window.location.pathname; + + // Check if current path is public + return !publicPaths.some((path) => { + if (path === "/") { + return currentPath === path; + } + return currentPath.startsWith(path); + }); +} + +/** + * Check authentication and redirect if necessary + */ +function checkAuth() { + // Only check auth for protected routes + if (!requiresAuth()) { + return; + } + + // Check if user is authenticated + if (!apiClient.isAuthenticated()) { + // Save the intended destination + const intendedUrl = window.location.pathname + window.location.search; + if (intendedUrl !== "/login") { + sessionStorage.setItem("intended_url", intendedUrl); + } + + // Redirect to login + window.location.href = "/login"; + return; + } +} + +/** + * Redirect to intended URL after login + */ +function redirectAfterLogin() { + const intendedUrl = sessionStorage.getItem("intended_url"); + sessionStorage.removeItem("intended_url"); + + if (intendedUrl && intendedUrl !== "/login") { + window.location.href = intendedUrl; + } else { + window.location.href = "/dashboard"; + } +} + +/** + * Redirect authenticated users away from auth pages + */ +function redirectIfAuthenticated() { + const authPaths = ["/login", "/register"]; + const currentPath = window.location.pathname; + + if (authPaths.includes(currentPath) && apiClient.isAuthenticated()) { + window.location.href = "/dashboard"; + } +} + +// Run auth check on page load +document.addEventListener("DOMContentLoaded", () => { + checkAuth(); + redirectIfAuthenticated(); +}); + +// Export functions for use in other modules +export { checkAuth, redirectAfterLogin, redirectIfAuthenticated, requiresAuth }; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index c629a0c..9ef638b 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -8,27 +8,27 @@ window.Alpine = Alpine; // Wait for DOM and API client to be ready before starting Alpine function startAlpine() { - // Check if API client is ready - if (window.apiClientReady) { + // Check if API client is ready + if (window.apiClientReady) { + Alpine.start(); + console.log("Alpine.js started with API client ready"); + } else { + // Wait a bit and try again + setTimeout(() => { + if (window.apiClientReady) { Alpine.start(); - console.log('Alpine.js started with API client ready'); - } else { - // Wait a bit and try again - setTimeout(() => { - if (window.apiClientReady) { - Alpine.start(); - console.log('Alpine.js started after API client ready'); - } else { - console.warn('Starting Alpine.js without API client'); - Alpine.start(); - } - }, 100); - } + console.log("Alpine.js started after API client ready"); + } else { + console.warn("Starting Alpine.js without API client"); + Alpine.start(); + } + }, 100); + } } // Wait for DOM to be ready before starting Alpine -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', startAlpine); +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", startAlpine); } else { - startAlpine(); + startAlpine(); } diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php deleted file mode 100644 index ef36007..0000000 --- a/resources/views/auth/confirm-password.blade.php +++ /dev/null @@ -1,33 +0,0 @@ - - -
-
-
-

{{ __('Confirm Password') }}

-

- {{ __('Please confirm your password before continuing.') }} -

-
- -
- @csrf - -
- -
- - - - {{ __('Confirm Password') }} - -
- - - -
-
-
diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php deleted file mode 100644 index e12fc70..0000000 --- a/resources/views/auth/forgot-password.blade.php +++ /dev/null @@ -1,38 +0,0 @@ - - -
-
-
-

{{ __('Forgot Password') }}

-

- {{ __('Enter your email to receive a password reset link') }}

-
- - @if (session('status')) -
- {{ session('status') }} -
- @endif - -
- @csrf - -
- -
- - - - {{ __('Send Password Reset Link') }} - -
- - - -
-
-
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index df95561..3bd5f75 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,23 +1,68 @@
+ class="bg-white dark:bg-gray-800 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden" + x-data="loginForm()">

{{ __('Login') }}

Sign in to your account

-
- @csrf - + + @if (request()->query('expired')) +
+

+ Your session has expired. Please login again. +

+
+ @endif + + +
+

+
+ + +
+

+
+ + +
- + +
- + + @if (Route::has('password.request')) {{ __('Forgot password?') }} @@ -26,11 +71,27 @@ class="text-xs text-blue-600 dark:text-blue-400 hover:underline">{{ __('Forgot p
- +
- {{ __('Sign In') }} + @if (Route::has('register')) @@ -45,4 +106,52 @@ class="text-blue-600 dark:text-blue-400 hover:underline font-medium">{{ __('Sign @endif
+ + diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 7939bc9..61820da 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -1,6 +1,7 @@
+ class="bg-white dark:bg-gray-800 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden" + x-data="registerForm()">

{{ __('Register') }}

@@ -9,31 +10,83 @@ class="bg-white dark:bg-gray-800 rounded-lg shadow-md border border-gray-200 dar

-
- @csrf - -
- -
+ +
+

+
- + +
+

+
+ + +
- + + +

+ Enter your preferred username or phone number +

- + +
- + +
- {{ __('Create Account') }} +
@@ -46,4 +99,51 @@ class="text-blue-600 dark:text-blue-400 hover:underline font-medium">{{ __('Sign
+ +
diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php deleted file mode 100644 index 3a9acf8..0000000 --- a/resources/views/auth/reset-password.blade.php +++ /dev/null @@ -1,46 +0,0 @@ - - -
-
-
-

{{ __('Reset Password') }}

-

{{ __('Enter your email and new password below.') }} -

-
- -
- @csrf - - - -
- -
- - -
- -
- - -
- -
- - - - {{ __('Reset Password') }} - -
- - - -
-
-
diff --git a/resources/views/auth/verify-email.blade.php b/resources/views/auth/verify-email.blade.php deleted file mode 100644 index 5f5b6a0..0000000 --- a/resources/views/auth/verify-email.blade.php +++ /dev/null @@ -1,38 +0,0 @@ - - -
-
-
-

{{ __('Verify Your Email Address') }} -

-

- {{ __('Before proceeding, please check your email for a verification link.') }}
- {{ __('If you did not receive the email, you can request another below.') }} -

-
- - @if (session('status') === 'verification-link-sent') -
- {{ __('A new verification link has been sent to your email address.') }} -
- @endif - -
- @csrf - - {{ __('Resend Verification Email') }} - -
- -
-
- @csrf - -
-
-
-
-
diff --git a/resources/views/components/layouts/app/header.blade.php b/resources/views/components/layouts/app/header.blade.php index 8249937..85c3699 100644 --- a/resources/views/components/layouts/app/header.blade.php +++ b/resources/views/components/layouts/app/header.blade.php @@ -1,5 +1,5 @@ -
+
@@ -20,11 +20,11 @@ class="p-2 rounded-md text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:
-
- @csrf - -
+
+ +
diff --git a/routes/auth.php b/routes/auth.php deleted file mode 100644 index 23a70bb..0000000 --- a/routes/auth.php +++ /dev/null @@ -1,34 +0,0 @@ -group(function () { - Route::get('register', [RegistrationController::class, 'create'])->name('register'); - Route::post('register', [RegistrationController::class, 'store']); - - Route::get('login', [LoginController::class, 'create'])->name('login'); - Route::post('login', [LoginController::class, 'store']); - - Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])->name('password.request'); - Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])->name('password.email'); - - Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])->name('password.reset'); - Route::post('reset-password', [NewPasswordController::class, 'store'])->name('password.store'); -}); - -Route::middleware('auth')->group(function () { - Route::get('verify-email', [VerificationController::class, 'notice'])->name('verification.notice'); - Route::post('verify-email', [VerificationController::class, 'store'])->middleware('throttle:6,1')->name('verification.store'); - Route::get('verify-email/{id}/{hash}', [VerificationController::class, 'verify'])->middleware(['signed', 'throttle:6,1'])->name('verification.verify'); - - Route::get('confirm-password', [ConfirmationController::class, 'create'])->name('password.confirm'); - Route::post('confirm-password', [ConfirmationController::class, 'store'])->name('confirmation.store'); -}); - -Route::post('logout', [LoginController::class, 'destroy'])->name('logout'); diff --git a/routes/web.php b/routes/web.php index f17cc6d..f20484b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,100 +1,109 @@ name('home'); -Route::middleware(['auth', 'verified'])->group(function () { - // CSRF Token refresh endpoint - Route::get('/csrf-token', function () { - return response()->json([ - 'csrf_token' => csrf_token() - ]); - }); - - // Dashboard - Route::get('dashboard', function () { - return view('dashboard'); - })->name('dashboard'); - - // Tasks - Route::get('tasks', function () { - return view('tasks.index'); - })->name('tasks.index'); - Route::get('tasks/create', function () { - return view('tasks.create'); - })->name('tasks.create'); - Route::get('tasks/{id}/edit', function ($id) { - return view('tasks.edit', ['id' => $id]); - })->name('tasks.edit'); - Route::get('tasks/{id}', function ($id) { - return view('tasks.show', ['id' => $id]); - })->name('tasks.show'); - - // Dealerships - Route::get('dealerships', function () { - return view('dealerships.index'); - })->name('dealerships.index'); - Route::get('dealerships/create', function () { - return view('dealerships.create'); - })->name('dealerships.create'); - Route::get('dealerships/{id}/edit', function ($id) { - return view('dealerships.edit', ['id' => $id]); - })->name('dealerships.edit'); - Route::get('dealerships/{id}', function ($id) { - return view('dealerships.show', ['id' => $id]); - })->name('dealerships.show'); - - // Users Management - Route::get('users', function () { - return view('users.index'); - })->name('users.index'); - Route::get('users/create', function () { - return view('users.create'); - })->name('users.create'); - Route::get('users/{id}/edit', function ($id) { - return view('users.edit', ['id' => $id]); - })->name('users.edit'); - Route::get('users/{id}', function ($id) { - return view('users.show', ['id' => $id]); - })->name('users.show'); - - // Links - Route::get('links', function () { - return view('links.index'); - })->name('links.index'); - - // Settings - Route::get('settings/profile', [Settings\ProfileController::class, 'edit'])->name('settings.profile.edit'); - Route::put('settings/profile', [Settings\ProfileController::class, 'update'])->name('settings.profile.update'); - Route::delete('settings/profile', [Settings\ProfileController::class, 'destroy'])->name('settings.profile.destroy'); - Route::get('settings/password', [Settings\PasswordController::class, 'edit'])->name('settings.password.edit'); - Route::put('settings/password', [Settings\PasswordController::class, 'update'])->name('settings.password.update'); - Route::get('settings/appearance', [Settings\AppearanceController::class, 'edit'])->name('settings.appearance.edit'); - Route::get('settings/system', function () { - return view('settings.system'); - })->name('settings.system.edit'); - Route::get('settings/bot-api', [Settings\BotApiController::class, 'edit'])->name('settings.bot-api.edit'); - - // API Routes for Settings - Route::prefix('api/settings')->group(function () { - Route::get('/', [Settings\BotApiController::class, 'index']); - Route::get('/{key}', [Settings\BotApiController::class, 'show']); - Route::put('/{key}', [Settings\BotApiController::class, 'update']); - Route::delete('/{key}', [Settings\BotApiController::class, 'destroy']); - Route::post('/bulk', [Settings\BotApiController::class, 'bulkUpdate']); - }); - - // API Proxy Routes - Route::prefix('api/proxy')->group(function () { - Route::any('{endpoint}', [App\Http\Controllers\ApiProxyController::class, 'proxy']) - ->where('endpoint', '.*'); - Route::post('upload/{endpoint}', [App\Http\Controllers\ApiProxyController::class, 'proxyUpload']) - ->where('endpoint', '.*'); - }); -}); - -require __DIR__.'/auth.php'; +// Auth pages (login, register, etc.) +Route::get('/login', function () { + return view('auth.login'); +})->name('login'); + +Route::get('/register', function () { + return view('auth.register'); +})->name('register'); + +Route::get('/forgot-password', function () { + return view('auth.forgot-password'); +})->name('password.request'); + +Route::get('/reset-password/{token}', function ($token) { + return view('auth.reset-password', ['token' => $token]); +})->name('password.reset'); + +// Dashboard +Route::get('/dashboard', function () { + return view('dashboard'); +})->name('dashboard'); + +// Tasks +Route::get('/tasks', function () { + return view('tasks.index'); +})->name('tasks.index'); + +Route::get('/tasks/create', function () { + return view('tasks.create'); +})->name('tasks.create'); + +Route::get('/tasks/{id}/edit', function ($id) { + return view('tasks.edit', ['id' => $id]); +})->name('tasks.edit'); + +Route::get('/tasks/{id}', function ($id) { + return view('tasks.show', ['id' => $id]); +})->name('tasks.show'); + +// Dealerships +Route::get('/dealerships', function () { + return view('dealerships.index'); +})->name('dealerships.index'); + +Route::get('/dealerships/create', function () { + return view('dealerships.create'); +})->name('dealerships.create'); + +Route::get('/dealerships/{id}/edit', function ($id) { + return view('dealerships.edit', ['id' => $id]); +})->name('dealerships.edit'); + +Route::get('/dealerships/{id}', function ($id) { + return view('dealerships.show', ['id' => $id]); +})->name('dealerships.show'); + +// Users Management +Route::get('/users', function () { + return view('users.index'); +})->name('users.index'); + +Route::get('/users/create', function () { + return view('users.create'); +})->name('users.create'); + +Route::get('/users/{id}/edit', function ($id) { + return view('users.edit', ['id' => $id]); +})->name('users.edit'); + +Route::get('/users/{id}', function ($id) { + return view('users.show', ['id' => $id]); +})->name('users.show'); + +// Links +Route::get('/links', function () { + return view('links.index'); +})->name('links.index'); + +// Settings +Route::get('/settings/profile', function () { + return view('settings.profile'); +})->name('settings.profile.edit'); + +Route::get('/settings/password', function () { + return view('settings.password'); +})->name('settings.password.edit'); + +Route::get('/settings/appearance', function () { + return view('settings.appearance'); +})->name('settings.appearance.edit'); + +Route::get('/settings/system', function () { + return view('settings.system'); +})->name('settings.system.edit'); + +Route::get('/settings/bot-api', function () { + return view('settings.bot-api'); +})->name('settings.bot-api.edit'); diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php deleted file mode 100644 index a272b9d..0000000 --- a/tests/Feature/Auth/AuthenticationTest.php +++ /dev/null @@ -1,41 +0,0 @@ -get('/login'); - - $response->assertStatus(200); -}); - -test('users can authenticate using the login screen', function () { - $user = User::factory()->create(); - - $response = $this->post('/login', [ - 'email' => $user->email, - 'password' => 'password', - ]); - - $this->assertAuthenticated(); - $response->assertRedirect(route('dashboard', absolute: false)); -}); - -test('users can not authenticate with invalid password', function () { - $user = User::factory()->create(); - - $this->post('/login', [ - 'email' => $user->email, - 'password' => 'wrong-password', - ]); - - $this->assertGuest(); -}); - -test('users can logout', function () { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/logout'); - - $this->assertGuest(); - $response->assertRedirect('/'); -}); diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php deleted file mode 100644 index 2bb2aea..0000000 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ /dev/null @@ -1,47 +0,0 @@ -unverified()->create(); - - $response = $this->actingAs($user)->get('/verify-email'); - - $response->assertStatus(200); -}); - -test('email can be verified', function () { - $user = User::factory()->unverified()->create(); - - Event::fake(); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - now()->addMinutes(60), - ['id' => $user->id, 'hash' => sha1($user->email)] - ); - - $response = $this->actingAs($user)->get($verificationUrl); - - Event::assertDispatched(Verified::class); - - expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); - $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); -}); - -test('email is not verified with invalid hash', function () { - $user = User::factory()->unverified()->create(); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - now()->addMinutes(60), - ['id' => $user->id, 'hash' => sha1('wrong-email')] - ); - - $this->actingAs($user)->get($verificationUrl); - - expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); -}); diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php deleted file mode 100644 index 8a42902..0000000 --- a/tests/Feature/Auth/PasswordConfirmationTest.php +++ /dev/null @@ -1,32 +0,0 @@ -create(); - - $response = $this->actingAs($user)->get('/confirm-password'); - - $response->assertStatus(200); -}); - -test('password can be confirmed', function () { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/confirm-password', [ - 'password' => 'password', - ]); - - $response->assertRedirect(); - $response->assertSessionHasNoErrors(); -}); - -test('password is not confirmed with invalid password', function () { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/confirm-password', [ - 'password' => 'wrong-password', - ]); - - $response->assertSessionHasErrors(); -}); diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php deleted file mode 100644 index 0504276..0000000 --- a/tests/Feature/Auth/PasswordResetTest.php +++ /dev/null @@ -1,60 +0,0 @@ -get('/forgot-password'); - - $response->assertStatus(200); -}); - -test('reset password link can be requested', function () { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class); -}); - -test('reset password screen can be rendered', function () { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class, function ($notification) { - $response = $this->get('/reset-password/'.$notification->token); - - $response->assertStatus(200); - - return true; - }); -}); - -test('password can be reset with valid token', function () { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { - $response = $this->post('/reset-password', [ - 'token' => $notification->token, - 'email' => $user->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect(route('login')); - - return true; - }); -}); diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php deleted file mode 100644 index 352ca78..0000000 --- a/tests/Feature/Auth/RegistrationTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/register'); - - $response->assertStatus(200); -}); - -test('new users can register', function () { - $response = $this->post('/register', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $this->assertAuthenticated(); - $response->assertRedirect(route('dashboard', absolute: false)); -}); diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php deleted file mode 100644 index 912ea87..0000000 --- a/tests/Feature/Settings/PasswordUpdateTest.php +++ /dev/null @@ -1,40 +0,0 @@ -create(); - - $response = $this - ->actingAs($user) - ->from('/settings/profile') - ->put('/settings/password', [ - 'current_password' => 'password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/settings/profile'); - - expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue(); -}); - -test('correct password must be provided to update password', function () { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->from('/settings/profile') - ->put('/settings/password', [ - 'current_password' => 'wrong-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); - - $response - ->assertSessionHasErrors('current_password') - ->assertRedirect('/settings/profile'); -}); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php deleted file mode 100644 index ba855f3..0000000 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ /dev/null @@ -1,62 +0,0 @@ -actingAs(User::factory()->create()); - - $this->get('/settings/profile')->assertOk(); -}); - -test('profile information can be updated', function () { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->put('/settings/profile', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/settings/profile'); - - $user->refresh(); - - expect($user->name)->toBe('Test User'); - expect($user->email)->toBe('test@example.com'); - expect($user->email_verified_at)->toBeNull(); -}); - -test('email verification status is unchanged when email address is unchanged', function () { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->put('/settings/profile', [ - 'name' => 'Test User', - 'email' => $user->email, - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/settings/profile'); - - expect($user->refresh()->email_verified_at)->not->toBeNull(); -}); - -test('user can delete their account', function () { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->delete('/settings/profile'); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/'); - - $this->assertGuest(); - expect($user->fresh())->toBeNull(); -}); From 08a1c43a09a40c21f8d23c196d0cae3704709529 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 28 Oct 2025 11:53:17 +0100 Subject: [PATCH 3/3] Revert "Initial commit with task details for issue #28" This reverts commit ae0c21b076ed361512225ea1bc6fb31e2d5966df. --- CLAUDE.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ca1981f..a4a5ebf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -236,13 +236,3 @@ composer test ## Дальнейшее развитие Проект готов к расширению функциональности управления задачами. Текущая база включает полную систему аутентификации и базовые настройки пользователя, что обеспечивает прочную основу для добавления модуля задач. - ---- - -Issue to solve: undefined -Your prepared branch: issue-28-59188d4d -Your prepared working directory: /tmp/gh-issue-solver-1761648298326 -Your forked repository: konard/TaskMateFrontend -Original repository (upstream): xierongchuan/TaskMateFrontend - -Proceed. \ No newline at end of file