diff --git a/thanasoft-back/app/Http/Controllers/Api/AuthController.php b/thanasoft-back/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..9fd413b --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,128 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', Password::min(8)], + + ]); + + $user = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => $data['password'], // hashed via User model cast + ]); + + $token = $user->createToken('api')->plainTextToken; + + return $this->sendResponse([ + 'user' => $user, + 'token' => $token, + ], 'User registered successfully.'); + + } catch (ValidationException $e) { + return $this->sendError('Validation Error.', $e->errors(), 422); + } catch (\Exception $e) { + return $this->sendError('Registration failed.', ['error' => $e->getMessage()], 500); + } + } + + public function login(Request $request): JsonResponse + { + try { + $credentials = $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'string'], + ]); + + /** @var User|null $user */ + $user = User::where('email', $credentials['email'])->first(); + + if (! $user || ! Hash::check($credentials['password'], $user->password)) { + return $this->sendError('Invalid credentials.', ['email' => ['The provided credentials are incorrect.']], 401); + } + + $token = $user->createToken('api')->plainTextToken; + + return $this->sendResponse([ + 'user' => $user, + 'token' => $token, + ], 'Login successful.'); + + } catch (ValidationException $e) { + return $this->sendError('Validation Error.', $e->errors(), 422); + } catch (\Exception $e) { + return $this->sendError('Login failed.', ['error' => $e->getMessage()], 500); + } + } + + public function me(Request $request): JsonResponse + { + try { + $user = $request->user(); + + if (!$user) { + return $this->sendError('Unauthenticated.', [], 401); + } + + return $this->sendResponse($user, 'User retrieved successfully.'); + + } catch (\Exception $e) { + return $this->sendError('Failed to retrieve user.', ['error' => $e->getMessage()], 500); + } + } + + public function logout(Request $request): JsonResponse + { + try { + $user = $request->user(); + + if (!$user) { + return $this->sendError('Unauthenticated.', [], 401); + } + + if ($user->currentAccessToken()) { + $user->currentAccessToken()->delete(); + } + + return $this->sendResponse([], 'Logged out successfully.'); + + } catch (\Exception $e) { + return $this->sendError('Logout failed.', ['error' => $e->getMessage()], 500); + } + } + + public function logoutAll(Request $request): JsonResponse + { + try { + $user = $request->user(); + + if (!$user) { + return $this->sendError('Unauthenticated.', [], 401); + } + + $user->tokens()->delete(); + + return $this->sendResponse([], 'Logged out from all devices successfully.'); + + } catch (\Exception $e) { + return $this->sendError('Logout failed.', ['error' => $e->getMessage()], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/BaseController.php b/thanasoft-back/app/Http/Controllers/Api/BaseController.php new file mode 100644 index 0000000..072f0ef --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/BaseController.php @@ -0,0 +1,44 @@ + true, + 'data' => $result, + 'message' => $message, + ]; + + return response()->json($response, 200); + } + + /** + * return error response. + * + * @return \Illuminate\Http\Response + */ + public function sendError($error, $errorMessages = [], $code = 404) + { + $response = [ + 'success' => false, + 'message' => $error, + ]; + + if(!empty($errorMessages)){ + $response['data'] = $errorMessages; + } + + return response()->json($response, $code); + } +} diff --git a/thanasoft-back/config/sanctum.php b/thanasoft-back/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/thanasoft-back/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/thanasoft-back/database/migrations/2025_10_02_161329_create_personal_access_tokens_table.php b/thanasoft-back/database/migrations/2025_10_02_161329_create_personal_access_tokens_table.php new file mode 100644 index 0000000..69af0ba --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_02_161329_create_personal_access_tokens_table.php @@ -0,0 +1,35 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php new file mode 100644 index 0000000..0fe4136 --- /dev/null +++ b/thanasoft-back/routes/api.php @@ -0,0 +1,25 @@ +group(function () { + Route::post('/register', [AuthController::class, 'register']); + Route::post('/login', [AuthController::class, 'login']); + + Route::middleware('auth:sanctum')->group(function () { + Route::get('/me', [AuthController::class, 'me']); + Route::post('/logout', [AuthController::class, 'logout']); + Route::post('/logout-all', [AuthController::class, 'logoutAll']); + }); +}); diff --git a/thanasoft-front/src/types/user.ts b/thanasoft-front/src/types/user.ts new file mode 100644 index 0000000..80debdd --- /dev/null +++ b/thanasoft-front/src/types/user.ts @@ -0,0 +1,8 @@ +export interface User { + id: number; + name: string; + email: string; + email_verified_at?: string | null; + created_at?: string; + updated_at?: string; +}