diff --git a/app/Helpers/ValidationHelper.php b/app/Helpers/ValidationHelper.php new file mode 100644 index 0000000..93eef8a --- /dev/null +++ b/app/Helpers/ValidationHelper.php @@ -0,0 +1,39 @@ + 'required|exists:users,email', + 'password' => 'required' + ]; + } + + public static function getUserDetailsRules() + { + return [ + 'id' => 'required|uuid' + ]; + } + + public static function getCreateUserRules() + { + return [ + 'email' => 'required|email|unique:users,email', + 'password' => 'nullable', + 'name' => 'required|string' + ]; + } + + public static function getUpdateUserRules() + { + return [ + 'id' => 'required|exists:users,id', + 'email' => 'required|email|unique:users,email', + 'name' => 'required|string' + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..2376735 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,35 @@ +authService = new AuthServices(); + } + public function login(Request $request) + { + $input = $request->all(); + $validator = Validator::make($input, ValidationHelper::getLoginRules()); + + if ($validator->fails()) { + return $this->sendError('Unprocessable Entity.', $validator->errors(), 422); + } + + try { + $token = $this->authService->login($input); + return $this->sendResponse('Login successfully', $token); + } catch (\Exception $e) { + return $this->sendError('Login failed.', $e->getMessage, 500); + } + } +} diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php new file mode 100644 index 0000000..9e4bde5 --- /dev/null +++ b/app/Http/Controllers/BaseController.php @@ -0,0 +1,31 @@ + true, + 'data' => $result, + 'message' => $message, + ]; + + return response()->json($response, $code); + } + + public function sendError($error, $errorMessages = [], $code = 500) + { + $response = [ + 'success' => false, + 'message' => $error, + ]; + + if (!empty($errorMessages)) { + $response['error'] = $errorMessages; + } + + return response()->json($response, $code); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..d62cc48 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,112 @@ +userService = new UserServices(); + } + + public function getUsers() + { + try { + $data = $this->userService->getUsers(); + return $this->sendResponse('Retrieved users successfully', $data); + } catch (\Exception $e) { + return $this->sendError('Retrieved users failed.', $e->getMessage, 500); + } + } + + public function getUser(Request $request, $user_id) + { + try { + $id = $user_id; + $validator = Validator::make( + [ + 'id' => $id + ], + [ + 'id' => 'required|uuid|exists:users,id' + ] + ); + + if ($validator->fails()) { + return $this->sendError('Unprocessable Entity.', $validator->errors(), 422); + } + + $data = $this->userService->getUser($id); + return $this->sendResponse('Retrieved user successfully', $data); + } catch (\Exception $e) { + return $this->sendError('Retrieved user failed.', $e->getMessage, 500); + } + } + + public function createUser(Request $request) + { + try { + $input = $request->all(); + $validator = Validator::make($input, ValidationHelper::getCreateUserRules()); + + if ($validator->fails()) { + return $this->sendError('Unprocessable Entity.', $validator->errors(), 422); + } + + $data = $this->userService->createUser($input); + return $this->sendResponse('Created user successfully', $data); + } catch (\Exception $e) { + return $this->sendError('Create user failed', $e->getMessage()); + } + } + + public function updateUser(Request $request, $user_id) + { + try { + $input = $request->all(); + $input['id'] = $user_id; + $validator = Validator::make($input, ValidationHelper::getUpdateUserRules()); + + if ($validator->fails()) { + return $this->sendError('Unprocessable Entity.', $validator->errors(), 422); + } + + $data = $this->userService->updateUser($input); + return $this->sendResponse('Updated user successfully', $data); + } catch (\Exception $e) { + return $this->sendError('Updated user failed.', $e->getMessage, 500); + } + } + + public function deleteUser(Request $request, $user_id) + { + try { + $id = $user_id; + $validator = Validator::make( + [ + 'id' => $id + ], + [ + 'id' => 'required|uuid|exists:users,id' + ] + ); + + if ($validator->fails()) { + return $this->sendError('Unprocessable Entity.', $validator->errors(), 422); + } + + $data = $this->userService->deleteUser($id); + return $this->sendResponse('Deleted user successfully'); + } catch (\Exception $e) { + return $this->sendError('Deleted user failed.', $e->getMessage, 500); + } + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d3722c2..1b76991 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -40,7 +40,7 @@ class Kernel extends HttpKernel ], 'api' => [ - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], diff --git a/app/Models/Project.php b/app/Models/Project.php new file mode 100644 index 0000000..37c5911 --- /dev/null +++ b/app/Models/Project.php @@ -0,0 +1,23 @@ +hasMany(Task::class, 'project_id'); + } +} diff --git a/app/Models/Task.php b/app/Models/Task.php new file mode 100644 index 0000000..7a40751 --- /dev/null +++ b/app/Models/Task.php @@ -0,0 +1,32 @@ +belongsTo(Project::class, 'project_id'); + } + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 8996368..7aa127c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,10 +7,14 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; +use Illuminate\Database\Eloquent\SoftDeletes; +// use App\Traits\Uuid; class User extends Authenticatable { - use HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable, SoftDeletes; + + public $incrementing = false; /** * The attributes that are mass assignable. @@ -18,6 +22,7 @@ class User extends Authenticatable * @var array */ protected $fillable = [ + 'id', 'name', 'email', 'password', @@ -39,6 +44,12 @@ class User extends Authenticatable * @var array */ protected $casts = [ + 'id' => 'string', 'email_verified_at' => 'datetime', ]; + + public function tasks() + { + return $this->hasMany(Task::class, 'user_id'); + } } diff --git a/app/Services/AuthServices.php b/app/Services/AuthServices.php new file mode 100644 index 0000000..05d1a85 --- /dev/null +++ b/app/Services/AuthServices.php @@ -0,0 +1,25 @@ + $input['email'], 'password' => $input['password']])) { + $user = auth()->user(); + $token = $user->createToken($user->id); + + return $token->plainTextToken; + } + } catch (\Exception $e) { + Log::error('(Error) Login failed. Error: ' . PHP_EOL . $e->getMessage()); + throw new \Exception('Login failed.'); + } + } +} \ No newline at end of file diff --git a/app/Services/UserServices.php b/app/Services/UserServices.php new file mode 100644 index 0000000..8ffa016 --- /dev/null +++ b/app/Services/UserServices.php @@ -0,0 +1,95 @@ +getMessage()); + throw new \Exception('Login failed.'); + } + } + + public function getUser($id) + { + try { + $user = User::find($id); + return $user; + } catch (\Exception $e) { + Log::error('(Error) Retrieve users failed. Error: ' . PHP_EOL . $e->getMessage()); + throw new \Exception('Retrieve users failed.'); + } + } + + public function createUser($input) + { + try { + DB::beginTransaction(); + + $uuid = (string) Str::uuid(); + $user = User::create( + [ + 'id' => $uuid, + 'email' => $input['email'], + 'name' => $input['name'], + 'password' => empty($input['password']) ? null : bcrypt($input['password']) + ] + ); + + DB::commit(); + return $user; + } catch (\Exception $e) { + DB::rollBack(); + Log::error('(Error) Create user failed. Error: ' . PHP_EOL . $e->getMessage()); + throw new \Exception('Create user failed.'); + } + } + + public function updateUser($input) + { + try { + DB::beginTransaction(); + + $user = User::find($input['id']); + $user->update( + Arr::only($input, app(User::class)->getFillable()) + ); + + DB::commit(); + return $user; + } catch (\Exception $e) { + DB::rollBack(); + Log::error('(Error) Update user failed. Error: ' . PHP_EOL . $e->getMessage()); + throw new \Exception('Update user failed.'); + } + } + + public function deleteUser($id) + { + try { + DB::beginTransaction(); + + $user = User::find($id); + $user->delete(); + + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + Log::error('(Error) Delete user failed. Error: ' . PHP_EOL . $e->getMessage()); + throw new \Exception('Delete user failed.'); + } + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index a3eb239..34109ed 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -15,6 +15,7 @@ class UserFactory extends Factory public function definition() { return [ + 'id' => (string) Str::uuid(), 'name' => $this->faker->name(), 'email' => $this->faker->unique()->safeEmail(), 'email_verified_at' => now(), diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index 621a24e..99daa45 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -14,13 +14,14 @@ class CreateUsersTable extends Migration public function up() { Schema::create('users', function (Blueprint $table) { - $table->id(); + $table->uuid('id')->primary(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); + $table->string('password')->nullable(); + $table->rememberToken()->nullable(); $table->timestamps(); + $table->softDeletes(); }); } diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php index 4315e16..d24fe13 100644 --- a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -15,7 +15,7 @@ public function up() { Schema::create('personal_access_tokens', function (Blueprint $table) { $table->id(); - $table->morphs('tokenable'); + $table->uuidMorphs('tokenable'); $table->string('name'); $table->string('token', 64)->unique(); $table->text('abilities')->nullable(); diff --git a/database/migrations/2022_09_15_013400_create_projects_table.php b/database/migrations/2022_09_15_013400_create_projects_table.php new file mode 100644 index 0000000..fd314d1 --- /dev/null +++ b/database/migrations/2022_09_15_013400_create_projects_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->string('name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('projects'); + } +} diff --git a/database/migrations/2022_09_15_013456_create_tasks_table.php b/database/migrations/2022_09_15_013456_create_tasks_table.php new file mode 100644 index 0000000..bf0a96b --- /dev/null +++ b/database/migrations/2022_09_15_013456_create_tasks_table.php @@ -0,0 +1,40 @@ +uuid('id'); + $table->string('title'); + $table->string('description')->nullable(); + $table->string('status'); + $table->foreignUuid('project_id'); + $table->foreignUuid('user_id'); + $table->foreign('project_id') + ->references('id')->on('projects'); + $table->foreign('user_id') + ->references('id')->on('users'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tasks'); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 57b73b5..2e681cd 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -13,6 +13,14 @@ class DatabaseSeeder extends Seeder */ public function run() { - // \App\Models\User::factory(10)->create(); + \App\Models\User::factory()->create( + [ + 'name' => 'admin', + 'email' => 'admin@test.my', + 'password' => bcrypt('password123') + ] + ); + + \App\Models\User::factory(5)->create(); } } diff --git a/routes/api.php b/routes/api.php index eb6fa48..fc8ff40 100644 --- a/routes/api.php +++ b/routes/api.php @@ -14,6 +14,31 @@ | */ -Route::middleware('auth:sanctum')->get('/user', function (Request $request) { - return $request->user(); -}); +// Route::middleware('auth:sanctum')->get('/user', function (Request $request) { +// return $request->user(); +// }); + +Route::group( + ['namespace' => 'Api', 'middleware' => 'auth:sanctum', 'as' => 'api.'], + function () { + Route::group( + ['prefix' => 'v1'], + function () { + Route::group( + ['prefix' => 'users'], + function () { + Route::get('/', [App\Http\Controllers\UserController::class, 'getUsers'])->name('get.users'); + Route::get('/{user_id}', [App\Http\Controllers\UserController::class, 'getUser'])->name('get.user'); + Route::post('/', [App\Http\Controllers\UserController::class, 'createUser'])->name('create.user'); + Route::put('/{user_id}', [App\Http\Controllers\UserController::class, 'updateUser'])->name('update.user'); + // PUT and PATCH method almost same + Route::patch('/{user_id}', [App\Http\Controllers\UserController::class, 'updateUser'])->name('update.user'); + Route::delete('/{user_id}', [App\Http\Controllers\UserController::class, 'deleteUser'])->name('delete.user'); + } + ); + } + ); + } +); + +Route::post('/v1/login', [App\Http\Controllers\Auth\LoginController::class, 'login'])->name('login');