diff --git a/.env.example b/.env.example index b7becba..9e5bc80 100644 --- a/.env.example +++ b/.env.example @@ -48,5 +48,6 @@ PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 +L5_SWAGGER_CONST_HOST=http://laravel_test/api/ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/app/Http/Controllers/Api/V1/Auth/AuthController.php b/app/Http/Controllers/Api/V1/Auth/AuthController.php new file mode 100644 index 0000000..08b6053 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Auth/AuthController.php @@ -0,0 +1,110 @@ +json([ + 'status' => Response::HTTP_UNAUTHORIZED, + 'message' => "Token Missmatch" + ], Response::HTTP_UNAUTHORIZED); + } + + # Issued Token Response # + protected function respondWithToken($token) + { + return response()->json([ + 'status' => Response::HTTP_OK, + 'access_token' => $token, + 'token_type' => 'bearer', + 'expires_in' => JWTAuth::factory()->getTTL() * 300 # expired in 5 hours + ], Response::HTTP_OK); + } + + /** + * @OA\Post( + * path="/login", + * summary="Login", + * operationId="authLogin", + * tags={"Authentication"}, + * @OA\RequestBody( + * required=true, + * description="Login", + * @OA\JsonContent( + * required={"username","password"}, + * @OA\Property(property="username", type="string", format="text", example="admin"), + * @OA\Property(property="password", type="string", format="password", example="admin"), + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function login(Request $request) + { + try { + # Validation Rules # + $credentials = $request->only('username','password'); + $validator = Validator::make($credentials, [ + 'username' => 'required|string|min:3|max:50', + 'password' => 'required|string|min:3|max:100', + ]); + + # Cred Validation # + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + # Token Issued # + if ($token = JWTAuth::attempt($credentials)) { + return $this->respondWithToken($token); + } + + return response()->json([ + 'status' => Response::HTTP_UNAUTHORIZED, + 'message' => "user not found" + ], Response::HTTP_UNAUTHORIZED); + + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'message' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + + } + +} diff --git a/app/Http/Controllers/Api/V1/Project/ProjectController.php b/app/Http/Controllers/Api/V1/Project/ProjectController.php new file mode 100644 index 0000000..99b94f1 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Project/ProjectController.php @@ -0,0 +1,440 @@ +all(); + $validator = Validator::make($credentials, [ + 'q' => 'string|max:100', + 'pageIndex' => 'integer', + 'pageSize' => 'integer', + 'sortBy' => 'string|max:100', + 'sortDirection' => 'string|max:4', + ]); + + # Cred Validation + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + # Set Limit Data + $limit = $request->pageSize?$request->pageSize:3; + + # Set Page Index (Start Record) + $offset = $request->pageIndex?$request->pageIndex:0; + + # Set sort data + $sortBy = $request->sortBy?$request->sortBy:'name'; + $sortDirection = $request->sortDirection?$request->sortDirection:'ASC'; + + # Query Data + $data = new Projects; + if ($request->q) { + $data = $data->where('name','LIKE','%'.$request->q.'%'); + } + + $data = $data->offset($offset) + ->limit($limit) + ->orderBy($sortBy,$sortDirection) + ->get(); + + $count = $data?count($data):0; + return response()->json([ + 'status' => Response::HTTP_OK, + 'total' => $count, + 'data' => $data + ], Response::HTTP_OK); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Post( + * path="/v1/project", + * summary="Create Projects", + * operationId="storeProjects", + * tags={"Projects"}, + * security={ {"Bearer": {} }}, + * @OA\RequestBody( + * required=true, + * description="Create Project", + * @OA\JsonContent( + * @OA\Schema(title = "Json Create Project"), + * example = { + "name": "Second Project", + * } + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function store(Request $request) + { + try { + $credentials = $request->only('name'); + $validator = Validator::make($credentials, [ + 'name' => 'required|string|unique:projects|max:150', + ]); + + # Cred Validation + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + $data = Projects::create(["name" => $request->name]); + + return response()->json([ + 'status' => Response::HTTP_CREATED, + 'message' => "Create Project Success", + 'data' => $data + ], Response::HTTP_CREATED); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Get( + * path="/v1/project/{id}", + * summary="Show Projects", + * operationId="showProjects", + * tags={"Projects"}, + * security={ {"Bearer": {} }}, + * + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function show($id) + { + try { + $data = Projects::find($id); + if ($data) { + return response()->json([ + 'status' => Response::HTTP_OK, + 'data' => $data + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response::HTTP_OK, + 'message' => "Data Not Found" + ], Response::HTTP_OK); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Patch( + * path="/v1/project/{id}", + * summary="Update Projects", + * operationId="updateProjects", + * tags={"Projects"}, + * security={ {"Bearer": {} }}, + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\RequestBody( + * required=true, + * description="Update Project", + * @OA\JsonContent( + * @OA\Schema(title = "Json Update Project"), + * example = { + "name": "Update Project", + * } + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function projectUpdate(Request $request, $id) + { + try { + $data = Projects::find($id); + if ($data) { + $credentials = $request->only('name'); + $validator = Validator::make($credentials, [ + 'name' => 'required|max:100|unique:projects', + ]); + + # Cred Validation + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + $data->name = $request->name; + $data->save(); + + return response()->json([ + 'status' => Response::HTTP_OK, + 'data' => "Update Project Success" + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response:: HTTP_OK, + 'message' => "Data Not Found" + ], Response:: HTTP_OK); + + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Delete( + * path="/v1/project/{id}", + * summary="Delete Project By Id", + * operationId="deleteProjectId", + * tags={"Projects"}, + * security={ {"Bearer": {} }}, + * + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function destroy($id) + { + try { + $data = Projects::find($id); + + if ($data) { + $data->delete(); + return response()->json([ + 'status' => Response::HTTP_OK, + 'message' => "delete project success" + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response:: HTTP_OK, + 'message' => "Data Not Found" + ], Response:: HTTP_OK); + + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } +} diff --git a/app/Http/Controllers/Api/V1/Task/TaskController.php b/app/Http/Controllers/Api/V1/Task/TaskController.php new file mode 100644 index 0000000..ae1fa96 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Task/TaskController.php @@ -0,0 +1,414 @@ +get(); + $count = $data?count($data):0; + return response()->json([ + 'status' => Response::HTTP_OK, + 'total' => $count, + 'data' => $data + ], Response::HTTP_OK); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Post( + * path="/v1/task", + * summary="Create Task", + * operationId="storeTask", + * tags={"Tasks"}, + * security={ {"Bearer": {} }}, + * @OA\RequestBody( + * required=true, + * description="Create Task", + * @OA\JsonContent( + * @OA\Schema(title = "Json Create Task"), + * example = { + "title": "First Task", + "description": "This is First Task", + "project_id": "", + "user_id": "", + * } + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function store(Request $request) + { + try { + $credentials = $request->only('title','description','project_id','user_id'); + $validator = Validator::make($credentials, [ + 'title' => 'required|string|max:50', + 'description' => 'required|string', + 'project_id' => 'required|string', + 'user_id' => 'required|string', + ]); + + # Cred Validation + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + # Check Primary key + if (!Projects::find($request->project_id)) { + return response()->json(['error' => "project_id not found", 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + if (!User::find($request->user_id)) { + return response()->json(['error' => "user_id not found", 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + $currentUser = JWTAuth::authenticate($request->token); + $data = Tasks::create([ + "title" => $request->title, + "description" => $request->description, + "status_id" => 1, + "project_id" => $request->project_id, + "user_id" => $request->user_id, + "created_by" => $currentUser->id + ]); + + return response()->json([ + 'status' => Response::HTTP_CREATED, + 'message' => "Create Task Success", + 'data' => $data + ], Response::HTTP_CREATED); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage()." | ".$e->getFile()." | ".$e->getLine() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Get( + * path="/v1/task/{id}", + * summary="Show Task", + * operationId="showTask", + * tags={"Tasks"}, + * security={ {"Bearer": {} }}, + * + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function show($id) + { + try { + $data = Tasks::with('status','user','project')->find($id); + if ($data) { + return response()->json([ + 'status' => Response::HTTP_OK, + 'data' => $data + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response::HTTP_OK, + 'message' => "Data Not Found" + ], Response::HTTP_OK); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Patch( + * path="/v1/task/{id}", + * summary="Update Task", + * operationId="updateTask", + * tags={"Tasks"}, + * security={ {"Bearer": {} }}, + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\RequestBody( + * required=true, + * description="Update Task", + * @OA\JsonContent( + * @OA\Schema(title = "Json Update Task"), + * example = { + "title": "First Task", + "description": "This is First Task", + "status_id": 1, + "project_id": "", + "user_id": "", + * } + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function taskUpdate(Request $request, $id) + { + try { + $data = Tasks::find($id); + if ($data) { + $credentials = $request->only('title','description','status_id','project_id','user_id'); + $validator = Validator::make($credentials, [ + 'title' => 'required|string|max:50', + 'description' => 'required|string', + 'status_id' => 'integer', + 'project_id' => 'string', + 'user_id' => 'string', + ]); + + # Cred Validation + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + # Get User Role + $currentUser = JWTAuth::authenticate($request->token); + $role_id = $currentUser->role_id; + + if ($request->status_id) { + if (!MasterStatus::find($request->status_id)) { + return response()->json(['error' => "status_id not found", 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + $data->status_id = $request->status_id; + } + + # Only Product Owner can change Project + if ($request->project_id) { + if ($role_id != 2) { + return response()->json(["status"=>Response::HTTP_UNAUTHORIZED,'message' => 'Unauthorized User'],Response::HTTP_UNAUTHORIZED); + } + if (!Projects::find($request->project_id)) { + return response()->json(['error' => "project_id not found", 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + $data->project_id = $request->project_id; + } + + # Only Product Owner can change user + if ($request->user_id) { + if ($role_id != 2) { + return response()->json(["status"=>Response::HTTP_UNAUTHORIZED,'message' => 'Unauthorized User'],Response::HTTP_UNAUTHORIZED); + } + if (!User::find($request->user_id)) { + return response()->json(['error' => "user_id not found", 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + $data->user_id = $request->user_id; + } + + $data->title = $request->title; + $data->description = $request->description; + $data->save(); + + return response()->json([ + 'status' => Response::HTTP_OK, + 'data' => "Update Task Success" + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response:: HTTP_OK, + 'message' => "Data Not Found" + ], Response:: HTTP_OK); + + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Delete( + * path="/v1/task/{id}", + * summary="Delete Task By Id", + * operationId="deleteTaskId", + * tags={"Tasks"}, + * security={ {"Bearer": {} }}, + * + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function destroy($id) + { + try { + $data = Tasks::find($id); + + if ($data) { + $data->delete(); + return response()->json([ + 'status' => Response::HTTP_OK, + 'message' => "delete task success" + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response:: HTTP_OK, + 'message' => "Data Not Found" + ], Response:: HTTP_OK); + + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } +} diff --git a/app/Http/Controllers/Api/V1/User/UserController.php b/app/Http/Controllers/Api/V1/User/UserController.php new file mode 100644 index 0000000..826e0a5 --- /dev/null +++ b/app/Http/Controllers/Api/V1/User/UserController.php @@ -0,0 +1,377 @@ +middleware('is.admin'); + } + + /** + * @OA\Get( + * path="/v1/user", + * summary="User", + * operationId="getUsers", + * tags={"Users"}, + * security={ {"Bearer": {} }}, + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function index() + { + try { + $data = User::with('role')->get(); + $count = $data?count($data):0; + return response()->json([ + 'status' => Response::HTTP_OK, + 'total' => $count, + 'data' => $data + ], Response::HTTP_OK); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Post( + * path="/v1/user", + * summary="Create Users", + * operationId="createUser", + * tags={"Users"}, + * security={ {"Bearer": {} }}, + * @OA\RequestBody( + * required=true, + * description="Create Users", + * @OA\JsonContent( + * @OA\Schema(title = "Json Create User"), + * example = { + "data": + { + { + "name": "User 1", + "username": "user1", + "password": "user1pwd", + "role_id" : 1 + } + } + * } + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function store(Request $request) + { + try { + $credentials = $request->only('data'); + $validator = Validator::make($credentials, [ + 'data' => 'required|array', + 'data.*.name' => 'required|max:50', + 'data.*.username' => 'required|max:100|unique:users', + 'data.*.password' => 'required|max:100', + 'data.*.role_id' => 'required|integer', + ]); + + # Cred Validation + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + foreach ($request->data as $key => $value) { + $value['password'] = bcrypt($value['password']); + User::create($value); + } + + return response()->json([ + 'status' => Response::HTTP_CREATED, + 'total' => count($request->data), + 'message' => "Create User Success" + ], Response::HTTP_CREATED); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Get( + * path="/v1/user/{id}", + * summary="Get User By Id", + * operationId="getUsersId", + * tags={"Users"}, + * security={ {"Bearer": {} }}, + * + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function show($id) + { + try { + $data = User::with('role')->find($id); + if ($data) { + return response()->json([ + 'status' => Response::HTTP_OK, + 'data' => $data + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response:: HTTP_OK, + 'message' => "Data Not Found" + ], Response:: HTTP_OK); + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Patch( + * path="/v1/user/{id}", + * summary="Edit User ", + * operationId="editUser", + * tags={"Users"}, + * security={ {"Bearer": {} }}, + * + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="1", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\RequestBody( + * required=true, + * description="Update User", + * @OA\JsonContent( + * @OA\Schema(title = "Json Update User"), + * example = { + "name": "User 1", + "username": "user1", + "password": "user1pwd", + "role_id" : 1 + * } + * ), + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function userUpdate(Request $request, $id) + { + try { + $data = User::find($id); + if ($data) { + $credentials = $request->all(); + $validator = Validator::make($credentials, [ + 'name' => 'required|max:50', + 'username' => 'required|max:100|unique:users', + 'password' => 'required|max:100', + 'role_id' => 'required|integer', + ]); + + # Cred Validation + if ($validator->fails()) { + return response()->json(['error' => $validator->errors(), 'status' => Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); + } + + $data->name = $request->name; + $data->username = $request->username; + $data->password = bcrypt($request->password); + $data->role_id = $request->role_id; + $data->save(); + + return response()->json([ + 'status' => Response::HTTP_OK, + 'data' => "Update User Success" + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response:: HTTP_OK, + 'message' => "Data Not Found" + ], Response:: HTTP_OK); + + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } + + /** + * @OA\Delete( + * path="/v1/user/{id}", + * summary="Delete User By Id", + * operationId="deleteUserId", + * tags={"Users"}, + * security={ {"Bearer": {} }}, + * + * @OA\Parameter( + * description="id", + * in="path", + * name="id", + * required=true, + * example="", + * @OA\Schema( + * type="string", + * format="text" + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful operation", + * ), + * @OA\Response( + * response=400, + * description="Bad Request", + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error", + * ) + * ) + */ + public function destroy($id) + { + try { + $data = User::find($id); + + if ($data) { + $data->delete(); + return response()->json([ + 'status' => Response::HTTP_OK, + 'message' => "delete user success" + ], Response::HTTP_OK); + } + + return response()->json([ + 'status' => Response:: HTTP_OK, + 'message' => "Data Not Found" + ], Response:: HTTP_OK); + + } catch (Exception $e) { + return response()->json([ + 'status' => Response::HTTP_BAD_REQUEST, + 'error' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index a0a2a8a..4b03980 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -7,6 +7,26 @@ use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; +/** + * @OA\Info( + * version="1.0.0", + * title="IT Business Solution - Laravel Test", + * description="Laravel Test Documentation", + * ) + * @OA\Server( + * url=L5_SWAGGER_CONST_HOST, + * description="Demo API Server" + * ) + + * @OA\SecurityScheme( + * securityScheme="Bearer", + * bearerFormat="JWT", + * type="apiKey", + * in="header", + * name="Authorization" + * ) +*/ + class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d3722c2..ef7540d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -63,5 +63,9 @@ class Kernel extends HttpKernel 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'jwt.verify' => \App\Http\Middleware\JwtMiddleware::class, + 'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken', + 'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken', + 'is.admin' => \App\Http\Middleware\IsAdmin::class ]; } diff --git a/app/Http/Middleware/IsAdmin.php b/app/Http/Middleware/IsAdmin.php new file mode 100644 index 0000000..9f04f07 --- /dev/null +++ b/app/Http/Middleware/IsAdmin.php @@ -0,0 +1,27 @@ +token); + if ($currentUser->role_id == 3) { + return $next($request); + } + return response()->json(["status"=>Response::HTTP_UNAUTHORIZED,'message' => 'Unauthorized User'],Response::HTTP_UNAUTHORIZED); + } +} diff --git a/app/Http/Middleware/IsProductOwner.php b/app/Http/Middleware/IsProductOwner.php new file mode 100644 index 0000000..48ae816 --- /dev/null +++ b/app/Http/Middleware/IsProductOwner.php @@ -0,0 +1,27 @@ +token); + if ($currentUser->role_id == 2) { + return $next($request); + } + return response()->json(["status"=>Response::HTTP_UNAUTHORIZED,'message' => 'Unauthorized User'],Response::HTTP_UNAUTHORIZED); + } +} diff --git a/app/Http/Middleware/JwtMiddleware.php b/app/Http/Middleware/JwtMiddleware.php new file mode 100644 index 0000000..f95bad9 --- /dev/null +++ b/app/Http/Middleware/JwtMiddleware.php @@ -0,0 +1,36 @@ +authenticate(); + } catch (Exception $e) { + if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException){ + return response()->json(['status' => 'Token is Invalid',Response::HTTP_UNAUTHORIZED]); + }else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException){ + return response()->json(['status' => 'Token is Expired'],Response::HTTP_UNAUTHORIZED); + }else{ + return response()->json(['status' => 'Authorization Token not found'],Response::HTTP_UNAUTHORIZED); + } + } + return $next($request); + } +} diff --git a/app/Models/MasterRole.php b/app/Models/MasterRole.php new file mode 100644 index 0000000..9300167 --- /dev/null +++ b/app/Models/MasterRole.php @@ -0,0 +1,13 @@ +belongsTo(MasterStatus::class, 'status_id', 'id'); + } + + public function project() + { + return $this->belongsTo(Projects::class, 'project_id', 'id'); + } + + public function user() + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 8996368..0e35655 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,15 +2,38 @@ namespace App\Models; +use App\Traits\UUID; +use Tymon\JWTAuth\Contracts\JWTSubject; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Laravel\Sanctum\HasApiTokens; +// use Laravel\Sanctum\HasApiTokens; +use App\Models\MasterRole; -class User extends Authenticatable +class User extends Authenticatable implements JWTSubject { - use HasApiTokens, HasFactory, Notifiable; + use HasFactory, Notifiable, UUID; + + /** + * Get the identifier that will be stored in the subject claim of the JWT. + * + * @return mixed + */ + public function getJWTIdentifier() + { + return $this->getKey(); + } + + /** + * Return a key value array, containing any custom claims to be added to the JWT. + * + * @return array + */ + public function getJWTCustomClaims() + { + return []; + } /** * The attributes that are mass assignable. @@ -19,8 +42,9 @@ class User extends Authenticatable */ protected $fillable = [ 'name', - 'email', + 'username', 'password', + 'role_id', ]; /** @@ -31,6 +55,8 @@ class User extends Authenticatable protected $hidden = [ 'password', 'remember_token', + 'created_at', + 'updated_at', ]; /** @@ -41,4 +67,14 @@ class User extends Authenticatable protected $casts = [ 'email_verified_at' => 'datetime', ]; + + /** + * Get the user that owns the User + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function role() + { + return $this->belongsTo(MasterRole::class, 'role_id', 'id'); + } } diff --git a/app/Traits/UUID.php b/app/Traits/UUID.php new file mode 100644 index 0000000..1242eec --- /dev/null +++ b/app/Traits/UUID.php @@ -0,0 +1,36 @@ +getKey() === null) { + $model->setAttribute($model->getKeyName(), Str::uuid()->toString()); + } + }); + } + + // Tells the database not to auto-increment this field + public function getIncrementing () + { + return false; + } + + // Helps the application specify the field type in the database + public function getKeyType () + { + return 'string'; + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 2b0c115..9ea9b70 100644 --- a/composer.json +++ b/composer.json @@ -6,11 +6,12 @@ "license": "MIT", "require": { "php": "^7.3|^8.0", + "darkaonline/l5-swagger": "8.3.*", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", "laravel/framework": "^8.75", - "laravel/sanctum": "^2.11", - "laravel/tinker": "^2.5" + "laravel/tinker": "^2.5", + "tymon/jwt-auth": "^1.0" }, "require-dev": { "facade/ignition": "^2.5", diff --git a/config/app.php b/config/app.php index a8d1a82..b231ab5 100644 --- a/config/app.php +++ b/config/app.php @@ -161,6 +161,7 @@ Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, + Tymon\JWTAuth\Providers\LaravelServiceProvider::class, /* * Package Service Providers... diff --git a/config/auth.php b/config/auth.php index d8c6cee..6e40bf5 100644 --- a/config/auth.php +++ b/config/auth.php @@ -14,7 +14,7 @@ */ 'defaults' => [ - 'guard' => 'web', + 'guard' => 'api', 'passwords' => 'users', ], @@ -36,8 +36,8 @@ */ 'guards' => [ - 'web' => [ - 'driver' => 'session', + 'api' => [ + 'driver' => 'jwt', 'provider' => 'users', ], ], diff --git a/config/jwt.php b/config/jwt.php new file mode 100644 index 0000000..8b7843b --- /dev/null +++ b/config/jwt.php @@ -0,0 +1,304 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + + /* + |-------------------------------------------------------------------------- + | JWT Authentication Secret + |-------------------------------------------------------------------------- + | + | Don't forget to set this in your .env file, as it will be used to sign + | your tokens. A helper command is provided for this: + | `php artisan jwt:secret` + | + | Note: This will be used for Symmetric algorithms only (HMAC), + | since RSA and ECDSA use a private/public key combo (See below). + | + */ + + 'secret' => env('JWT_SECRET'), + + /* + |-------------------------------------------------------------------------- + | JWT Authentication Keys + |-------------------------------------------------------------------------- + | + | The algorithm you are using, will determine whether your tokens are + | signed with a random string (defined in `JWT_SECRET`) or using the + | following public & private keys. + | + | Symmetric Algorithms: + | HS256, HS384 & HS512 will use `JWT_SECRET`. + | + | Asymmetric Algorithms: + | RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below. + | + */ + + 'keys' => [ + + /* + |-------------------------------------------------------------------------- + | Public Key + |-------------------------------------------------------------------------- + | + | A path or resource to your public key. + | + | E.g. 'file://path/to/public/key' + | + */ + + 'public' => env('JWT_PUBLIC_KEY'), + + /* + |-------------------------------------------------------------------------- + | Private Key + |-------------------------------------------------------------------------- + | + | A path or resource to your private key. + | + | E.g. 'file://path/to/private/key' + | + */ + + 'private' => env('JWT_PRIVATE_KEY'), + + /* + |-------------------------------------------------------------------------- + | Passphrase + |-------------------------------------------------------------------------- + | + | The passphrase for your private key. Can be null if none set. + | + */ + + 'passphrase' => env('JWT_PASSPHRASE'), + + ], + + /* + |-------------------------------------------------------------------------- + | JWT time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token will be valid for. + | Defaults to 1 hour. + | + | You can also set this to null, to yield a never expiring token. + | Some people may want this behaviour for e.g. a mobile app. + | This is not particularly recommended, so make sure you have appropriate + | systems in place to revoke the token if necessary. + | Notice: If you set this to null you should remove 'exp' element from 'required_claims' list. + | + */ + + 'ttl' => env('JWT_TTL', 60), + + /* + |-------------------------------------------------------------------------- + | Refresh time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token can be refreshed + | within. I.E. The user can refresh their token within a 2 week window of + | the original token being created until they must re-authenticate. + | Defaults to 2 weeks. + | + | You can also set this to null, to yield an infinite refresh time. + | Some may want this instead of never expiring tokens for e.g. a mobile app. + | This is not particularly recommended, so make sure you have appropriate + | systems in place to revoke the token if necessary. + | + */ + + 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), + + /* + |-------------------------------------------------------------------------- + | JWT hashing algorithm + |-------------------------------------------------------------------------- + | + | Specify the hashing algorithm that will be used to sign the token. + | + | See here: https://github.com/namshi/jose/tree/master/src/Namshi/JOSE/Signer/OpenSSL + | for possible values. + | + */ + + 'algo' => env('JWT_ALGO', 'HS256'), + + /* + |-------------------------------------------------------------------------- + | Required Claims + |-------------------------------------------------------------------------- + | + | Specify the required claims that must exist in any token. + | A TokenInvalidException will be thrown if any of these claims are not + | present in the payload. + | + */ + + 'required_claims' => [ + 'iss', + 'iat', + 'exp', + 'nbf', + 'sub', + 'jti', + ], + + /* + |-------------------------------------------------------------------------- + | Persistent Claims + |-------------------------------------------------------------------------- + | + | Specify the claim keys to be persisted when refreshing a token. + | `sub` and `iat` will automatically be persisted, in + | addition to the these claims. + | + | Note: If a claim does not exist then it will be ignored. + | + */ + + 'persistent_claims' => [ + // 'foo', + // 'bar', + ], + + /* + |-------------------------------------------------------------------------- + | Lock Subject + |-------------------------------------------------------------------------- + | + | This will determine whether a `prv` claim is automatically added to + | the token. The purpose of this is to ensure that if you have multiple + | authentication models e.g. `App\User` & `App\OtherPerson`, then we + | should prevent one authentication request from impersonating another, + | if 2 tokens happen to have the same id across the 2 different models. + | + | Under specific circumstances, you may want to disable this behaviour + | e.g. if you only have one authentication model, then you would save + | a little on token size. + | + */ + + 'lock_subject' => true, + + /* + |-------------------------------------------------------------------------- + | Leeway + |-------------------------------------------------------------------------- + | + | This property gives the jwt timestamp claims some "leeway". + | Meaning that if you have any unavoidable slight clock skew on + | any of your servers then this will afford you some level of cushioning. + | + | This applies to the claims `iat`, `nbf` and `exp`. + | + | Specify in seconds - only if you know you need it. + | + */ + + 'leeway' => env('JWT_LEEWAY', 0), + + /* + |-------------------------------------------------------------------------- + | Blacklist Enabled + |-------------------------------------------------------------------------- + | + | In order to invalidate tokens, you must have the blacklist enabled. + | If you do not want or need this functionality, then set this to false. + | + */ + + 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), + + /* + | ------------------------------------------------------------------------- + | Blacklist Grace Period + | ------------------------------------------------------------------------- + | + | When multiple concurrent requests are made with the same JWT, + | it is possible that some of them fail, due to token regeneration + | on every request. + | + | Set grace period in seconds to prevent parallel request failure. + | + */ + + 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0), + + /* + |-------------------------------------------------------------------------- + | Cookies encryption + |-------------------------------------------------------------------------- + | + | By default Laravel encrypt cookies for security reason. + | If you decide to not decrypt cookies, you will have to configure Laravel + | to not encrypt your cookie token by adding its name into the $except + | array available in the middleware "EncryptCookies" provided by Laravel. + | see https://laravel.com/docs/master/responses#cookies-and-encryption + | for details. + | + | Set it to true if you want to decrypt cookies. + | + */ + + 'decrypt_cookies' => false, + + /* + |-------------------------------------------------------------------------- + | Providers + |-------------------------------------------------------------------------- + | + | Specify the various providers used throughout the package. + | + */ + + 'providers' => [ + + /* + |-------------------------------------------------------------------------- + | JWT Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to create and decode the tokens. + | + */ + + 'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class, + + /* + |-------------------------------------------------------------------------- + | Authentication Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to authenticate users. + | + */ + + 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class, + + /* + |-------------------------------------------------------------------------- + | Storage Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to store tokens in the blacklist. + | + */ + + 'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class, + + ], + +]; diff --git a/config/l5-swagger.php b/config/l5-swagger.php new file mode 100644 index 0000000..b540d6c --- /dev/null +++ b/config/l5-swagger.php @@ -0,0 +1,294 @@ + 'default', + 'documentations' => [ + 'default' => [ + 'api' => [ + 'title' => 'L5 Swagger UI', + ], + + 'routes' => [ + /* + * Route for accessing api documentation interface + */ + 'api' => 'api/documentation', + ], + 'paths' => [ + /* + * Edit to include full URL in ui for assets + */ + 'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true), + + /* + * File name of the generated json documentation file + */ + 'docs_json' => 'api-docs.json', + + /* + * File name of the generated YAML documentation file + */ + 'docs_yaml' => 'api-docs.yaml', + + /* + * Set this to `json` or `yaml` to determine which documentation file to use in UI + */ + 'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'), + + /* + * Absolute paths to directory containing the swagger annotations are stored. + */ + 'annotations' => [ + base_path('app'), + ], + + ], + ], + ], + 'defaults' => [ + 'routes' => [ + /* + * Route for accessing parsed swagger annotations. + */ + 'docs' => 'docs', + + /* + * Route for Oauth2 authentication callback. + */ + 'oauth2_callback' => 'api/oauth2-callback', + + /* + * Middleware allows to prevent unexpected access to API documentation + */ + 'middleware' => [ + 'api' => [], + 'asset' => [], + 'docs' => [], + 'oauth2_callback' => [], + ], + + /* + * Route Group options + */ + 'group_options' => [], + ], + + 'paths' => [ + /* + * Absolute path to location where parsed annotations will be stored + */ + 'docs' => storage_path('api-docs'), + + /* + * Absolute path to directory where to export views + */ + 'views' => base_path('resources/views/vendor/l5-swagger'), + + /* + * Edit to set the api's base path + */ + 'base' => env('L5_SWAGGER_BASE_PATH', null), + + /* + * Edit to set path where swagger ui assets should be stored + */ + 'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'), + + /* + * Absolute path to directories that should be exclude from scanning + * @deprecated Please use `scanOptions.exclude` + * `scanOptions.exclude` overwrites this + */ + 'excludes' => [], + ], + + 'scanOptions' => [ + /** + * analyser: defaults to \OpenApi\StaticAnalyser . + * + * @see \OpenApi\scan + */ + 'analyser' => null, + + /** + * analysis: defaults to a new \OpenApi\Analysis . + * + * @see \OpenApi\scan + */ + 'analysis' => null, + + /** + * Custom query path processors classes. + * + * @link https://github.com/zircote/swagger-php/tree/master/Examples/schema-query-parameter-processor + * @see \OpenApi\scan + */ + 'processors' => [ + // new \App\SwaggerProcessors\SchemaQueryParameter(), + ], + + /** + * pattern: string $pattern File pattern(s) to scan (default: *.php) . + * + * @see \OpenApi\scan + */ + 'pattern' => null, + + /* + * Absolute path to directories that should be exclude from scanning + * @note This option overwrites `paths.excludes` + * @see \OpenApi\scan + */ + 'exclude' => [], + ], + + /* + * API security definitions. Will be generated into documentation file. + */ + 'securityDefinitions' => [ + 'securitySchemes' => [ + /* + * Examples of Security schemes + */ + /* + 'api_key_security_example' => [ // Unique name of security + 'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'A short description for security scheme', + 'name' => 'api_key', // The name of the header or query parameter to be used. + 'in' => 'header', // The location of the API key. Valid values are "query" or "header". + ], + 'oauth2_security_example' => [ // Unique name of security + 'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'A short description for oauth2 security scheme.', + 'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode". + 'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode) + //'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode) + 'scopes' => [ + 'read:projects' => 'read your projects', + 'write:projects' => 'modify projects in your account', + ] + ], + */ + + /* Open API 3.0 support + 'passport' => [ // Unique name of security + 'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'Laravel passport oauth2 security.', + 'in' => 'header', + 'scheme' => 'https', + 'flows' => [ + "password" => [ + "authorizationUrl" => config('app.url') . '/oauth/authorize', + "tokenUrl" => config('app.url') . '/oauth/token', + "refreshUrl" => config('app.url') . '/token/refresh', + "scopes" => [] + ], + ], + ], + 'sanctum' => [ // Unique name of security + 'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'Enter token in format (Bearer )', + 'name' => 'Authorization', // The name of the header or query parameter to be used. + 'in' => 'header', // The location of the API key. Valid values are "query" or "header". + ], + */ + ], + 'security' => [ + /* + * Examples of Securities + */ + [ + /* + 'oauth2_security_example' => [ + 'read', + 'write' + ], + + 'passport' => [] + */ + ], + ], + ], + + /* + * Set this to `true` in development mode so that docs would be regenerated on each request + * Set this to `false` to disable swagger generation on production + */ + 'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false), + + /* + * Set this to `true` to generate a copy of documentation in yaml format + */ + 'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false), + + /* + * Edit to trust the proxy's ip address - needed for AWS Load Balancer + * string[] + */ + 'proxy' => false, + + /* + * Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle. + * See more at: https://github.com/swagger-api/swagger-ui#configs-plugin + */ + 'additional_config_url' => null, + + /* + * Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically), + * 'method' (sort by HTTP method). + * Default is the order returned by the server unchanged. + */ + 'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null), + + /* + * Pass the validatorUrl parameter to SwaggerUi init on the JS side. + * A null value here disables validation. + */ + 'validator_url' => null, + + /* + * Swagger UI configuration parameters + */ + 'ui' => [ + 'display' => [ + /* + * Controls the default expansion setting for the operations and tags. It can be : + * 'list' (expands only the tags), + * 'full' (expands the tags and operations), + * 'none' (expands nothing). + */ + 'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'), + + /** + * If set, enables filtering. The top bar will show an edit box that + * you can use to filter the tagged operations that are shown. Can be + * Boolean to enable or disable, or a string, in which case filtering + * will be enabled using that string as the filter expression. Filtering + * is case-sensitive matching the filter expression anywhere inside + * the tag. + */ + 'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false + ], + + 'authorization' => [ + /* + * If set to true, it persists authorization data, and it would not be lost on browser close/refresh + */ + 'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false), + + 'oauth2' => [ + /* + * If set to true, adds PKCE to AuthorizationCodeGrant flow + */ + 'use_pkce_with_authorization_code_grant' => false, + ], + ], + ], + /* + * Constants which can be used in annotations + */ + 'constants' => [ + 'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'), + ], + ], +]; diff --git a/database/migrations/2013_09_20_075849_master_role.php b/database/migrations/2013_09_20_075849_master_role.php new file mode 100644 index 0000000..2d2d8be --- /dev/null +++ b/database/migrations/2013_09_20_075849_master_role.php @@ -0,0 +1,31 @@ +id(); + $table->string('name', 150)->unique(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('master_role'); + } +} diff --git a/database/migrations/2013_09_20_075902_master_status.php b/database/migrations/2013_09_20_075902_master_status.php new file mode 100644 index 0000000..f781265 --- /dev/null +++ b/database/migrations/2013_09_20_075902_master_status.php @@ -0,0 +1,31 @@ +id(); + $table->string('name', 150)->unique(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('master_status'); + } +} 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..c016337 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->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); + $table->uuid('id')->primary(); + $table->string('name', 50); + $table->string('username', 100)->unique(); + $table->string('password', 100); + $table->unsignedBigInteger('role_id')->index('role_id'); $table->timestamps(); + + $table->foreign('role_id')->references('id')->on('master_role'); }); } diff --git a/database/migrations/2022_09_20_070859_create_projects_table.php b/database/migrations/2022_09_20_070859_create_projects_table.php new file mode 100644 index 0000000..aa943e5 --- /dev/null +++ b/database/migrations/2022_09_20_070859_create_projects_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->string('name', 150)->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('projects'); + } +} diff --git a/database/migrations/2022_09_20_070919_create_tasks_table.php b/database/migrations/2022_09_20_070919_create_tasks_table.php new file mode 100644 index 0000000..1d6700c --- /dev/null +++ b/database/migrations/2022_09_20_070919_create_tasks_table.php @@ -0,0 +1,43 @@ +uuid('id')->primary(); + $table->string('title', 50); + $table->string('description'); + $table->unsignedBigInteger('status_id'); + $table->uuid('project_id')->index('project_id'); + $table->uuid('user_id')->index('user_id'); + $table->uuid('created_by')->index('created_by'); + $table->timestamps(); + + $table->foreign('project_id')->references('id')->on('projects'); + $table->foreign('user_id')->references('id')->on('users'); + $table->foreign('created_by')->references('id')->on('users'); + $table->foreign('status_id')->references('id')->on('master_status'); + + }); + } + + /** + * 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..77c7402 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; +use App\Models\User; class DatabaseSeeder extends Seeder { @@ -13,6 +14,33 @@ class DatabaseSeeder extends Seeder */ public function run() { - // \App\Models\User::factory(10)->create(); + + $this->call([ + MasterRole::class, + MasterStatus::class, + ]); + + # Default User + $users = [[ + "name" => "Superuser", + "username" => "admin", + "password" => bcrypt("123456"), + "role_id" => 3 + ],[ + "name" => "Product Owner", + "username" => "product", + "password" => bcrypt("123456"), + "role_id" => 2 + ],[ + "name" => "Team Member", + "username" => "member", + "password" => bcrypt("123456"), + "role_id" => 1 + ]]; + + foreach ($users as $key => $value) { + User::create($value); + } + } } diff --git a/database/seeders/MasterRole.php b/database/seeders/MasterRole.php new file mode 100644 index 0000000..c9ed2d2 --- /dev/null +++ b/database/seeders/MasterRole.php @@ -0,0 +1,29 @@ +insert([ + [ + "name" => "MEMBER" + ], + [ + "name" => "PRODUCT_OWNER" + ], + [ + "name" => "ADMIN" + ] + ]); + } +} diff --git a/database/seeders/MasterStatus.php b/database/seeders/MasterStatus.php new file mode 100644 index 0000000..a73c9a8 --- /dev/null +++ b/database/seeders/MasterStatus.php @@ -0,0 +1,32 @@ +insert([ + [ + "name" => "NOT_STARTED" + ], + [ + "name" => "IN_PROGRESS" + ], + [ + "name" => "READY_FOR_TEST" + ], + [ + "name" => "COMPLETED" + ] + ]); + } +} diff --git a/docs/Laravel Test.postman_collection.json b/docs/Laravel Test.postman_collection.json new file mode 100644 index 0000000..7687c14 --- /dev/null +++ b/docs/Laravel Test.postman_collection.json @@ -0,0 +1,467 @@ +{ + "info": { + "_postman_id": "f75b2b3e-c3f4-4bcf-aa72-aac938eccc60", + "name": "Laravel Test", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "1899841" + }, + "item": [ + { + "name": "Users", + "item": [ + { + "name": "Get All User", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_local}}/v1/user", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "user" + ] + } + }, + "response": [] + }, + { + "name": "Get User", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_local}}/v1/user/3410d735-4b21-4859-ad32-5b3f92c1819f", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "user", + "3410d735-4b21-4859-ad32-5b3f92c1819f" + ] + } + }, + "response": [] + }, + { + "name": "Store User", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\":[\r\n {\r\n \"name\" : \"Member\",\r\n \"username\" : \"member\",\r\n \"password\" : \"123456\",\r\n \"role_id\" : \"1\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/user", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "user" + ] + } + }, + "response": [] + }, + { + "name": "Update User", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\" : \"user ABCDs\",\r\n \"username\" : \"userabcde\",\r\n \"password\" : \"userabcpwds\",\r\n \"role_id\" : \"1\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/user/a27bae31-5383-4f45-a469-cc4f38381b5a", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "user", + "a27bae31-5383-4f45-a469-cc4f38381b5a" + ] + } + }, + "response": [] + }, + { + "name": "Update User Idempotency", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\" : \"testing1\",\r\n \"username\" : \"testing1\",\r\n \"password\" : \"testing1\",\r\n \"role_id\" : \"1\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/user/3410d735-4b21-4859-ad32-5b3f92c1819f", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "user", + "3410d735-4b21-4859-ad32-5b3f92c1819f" + ] + } + }, + "response": [] + }, + { + "name": "Delete User", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{api_local}}/v1/user/3410d735-4b21-4859-ad32-5b3f92c1819f", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "user", + "3410d735-4b21-4859-ad32-5b3f92c1819f" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Projects", + "item": [ + { + "name": "Get All Project", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"q\" : \"-\",\r\n \"pageIndex\" : 0,\r\n \"pageSize\" : 1,\r\n \"sortDirection\" : \"DESC\",\r\n \"sortBy\" : \"name\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/project", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "project" + ] + } + }, + "response": [] + }, + { + "name": "Get Project", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_local}}/v1/project/04e659d2-26b3-4a5e-921b-80c8292e1cd6s", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "project", + "04e659d2-26b3-4a5e-921b-80c8292e1cd6s" + ] + } + }, + "response": [] + }, + { + "name": "Delete Project", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{api_local}}/v1/project/04e659d2-26b3-4a5e-921b-80c8292e1cd6", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "project", + "04e659d2-26b3-4a5e-921b-80c8292e1cd6" + ] + } + }, + "response": [] + }, + { + "name": "Update Project", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\" : \"ereses\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/project/04e659d2-26b3-4a5e-921b-80c8292e1cd6", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "project", + "04e659d2-26b3-4a5e-921b-80c8292e1cd6" + ] + } + }, + "response": [] + }, + { + "name": "Store Project", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\" : \"First Project\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/project", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "project" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Tasks", + "item": [ + { + "name": "Get All Tasks", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_local}}/v1/task", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "task" + ] + } + }, + "response": [] + }, + { + "name": "Store Task", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"title\" : \"First Task\",\r\n \"description\" : \"This is First Task\",\r\n \"project_id\" : \"16bc0986-efe4-45bb-ba3a-b9e2215eed79\",\r\n \"user_id\" : \"eb2f3ff8-063b-472b-b528-d1440b2f2126\",\r\n \"status_id\" : 1\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/task", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "task" + ] + } + }, + "response": [] + }, + { + "name": "Get Task", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_local}}/v1/task/27bf3305-3b07-42c4-9000-54ea42407daa", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "task", + "27bf3305-3b07-42c4-9000-54ea42407daa" + ] + } + }, + "response": [] + }, + { + "name": "Delete Task", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{api_local}}/v1/task/97dd8c27-657c-4ad4-8153-bd27c5854736", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "task", + "97dd8c27-657c-4ad4-8153-bd27c5854736" + ] + } + }, + "response": [] + }, + { + "name": "Update Task", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"title\" : \"Update Task\",\r\n \"description\" : \"This is Update Task\",\r\n \"project_id\" : \"16bc0986-efe4-45bb-ba3a-b9e2215eed79\",\r\n \"user_id\" : \"eb2f3ff8-063b-472b-b528-d1440b2f2126\",\r\n \"status_id\" : 2\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_local}}/v1/task/2d5e8841-e41e-4cf3-90c0-e61e11c363b7", + "host": [ + "{{api_local}}" + ], + "path": [ + "v1", + "task", + "2d5e8841-e41e-4cf3-90c0-e61e11c363b7" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "username", + "value": "admin", + "type": "text" + }, + { + "key": "password", + "value": "admin", + "type": "text" + } + ] + }, + "url": { + "raw": "{{api_local}}/login", + "host": [ + "{{api_local}}" + ], + "path": [ + "login" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sYXJhdmVsX3Rlc3RcL2FwaVwvbG9naW4iLCJpYXQiOjE2NjM4MzU1MzcsImV4cCI6MTY2MzgzOTEzNywibmJmIjoxNjYzODM1NTM3LCJqdGkiOiJYb3VRNllHR1lpc2J5alk0Iiwic3ViIjoiMGE5YjBjMTMtNDRlOC00OTI4LWE1MTctNmQ4NGZkYmEyNTU0IiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyJ9.GFWU_oi0q4is7MvlCILTxsyaXQTXgGrMGeI4SQclkVc", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "api_local", + "value": "laravel_test/api", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/docs/MyReadme.md b/docs/MyReadme.md new file mode 100644 index 0000000..18c7ce9 --- /dev/null +++ b/docs/MyReadme.md @@ -0,0 +1,14 @@ +# Setup Steps +1. Set virtual host as laravel_test in http / you can edit URL on L5_SWAGGER_CONST_HOST=http://laravel_test/api/ + +2. generate new application key for the first time +- $ php artisan key:generate + +3. generate new jwt key oauth for the first time +- $ php artisan jwt:secret + +4. caching new config +- $ php artisan config:cache + +5. generate l5-swagger +- $ php artisan l5-swagger:generate \ No newline at end of file diff --git a/resources/views/vendor/l5-swagger/.gitkeep b/resources/views/vendor/l5-swagger/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/vendor/l5-swagger/index.blade.php b/resources/views/vendor/l5-swagger/index.blade.php new file mode 100644 index 0000000..9fb36f5 --- /dev/null +++ b/resources/views/vendor/l5-swagger/index.blade.php @@ -0,0 +1,79 @@ + + + + + {{config('l5-swagger.documentations.'.$documentation.'.api.title')}} + + + + + + + +
+ + + + + + diff --git a/routes/api.php b/routes/api.php index eb6fa48..d1c3675 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,18 +2,56 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; - +use App\Http\Controllers\Api\V1\Auth\AuthController; +use App\Http\Controllers\Api\V1\User\UserController; +use App\Http\Controllers\Api\V1\Project\ProjectController; +use App\Http\Controllers\Api\V1\Task\TaskController; +use App\Http\Middleware\IsProductOwner; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- -| -| Here is where you can register API routes for your application. These -| routes are loaded by the RouteServiceProvider within a group which -| is assigned the "api" middleware group. Enjoy building your API! -| */ -Route::middleware('auth:sanctum')->get('/user', function (Request $request) { - return $request->user(); +# >>> Auth # + +Route::get('/', function () { + return "welcome to api"; }); + +Route::get('login', [AuthController::class, 'invalidLogin'])->name('login'); +Route::post('login', [AuthController::class, 'login']); +Route::post('register', [AuthController::class, 'register']); +# <<< Auth # + + +Route::group(['middleware' => ['jwt.verify']], function() { + Route::prefix('v1')->group(function () { + # >>> User Route # + Route::resource('user',UserController::class)->only([ + 'index', 'show', 'store', 'destroy' + ]); + Route::put('user/{id}', [UserController::class, 'userPut']); + Route::patch('user/{id}', [UserController::class, 'userUpdate']); + + # >>> Project Route # + Route::resource('project',ProjectController::class)->only([ + 'index', 'show', 'destroy' + ]); + Route::post('project', [ProjectController::class, 'store'])->middleware(IsProductOwner::class); + Route::put('project/{id}', [ProjectController::class, 'projectPut']); + Route::patch('project/{id}', [ProjectController::class, 'projectUpdate']); + + # >>> Task Route # + Route::resource('task',TaskController::class)->only([ + 'index', 'show', 'destroy' + ]); + Route::post('task', [TaskController::class, 'store'])->middleware(IsProductOwner::class); + Route::put('task/{id}', [TaskController::class, 'taskPut']); + Route::patch('task/{id}', [TaskController::class, 'taskUpdate']); + + + }); + + +}); \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index b130397..0112cba 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,5 +14,5 @@ */ Route::get('/', function () { - return view('welcome'); + return redirect('/api/documentation'); }); diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json new file mode 100644 index 0000000..e67ba35 --- /dev/null +++ b/storage/api-docs/api-docs.json @@ -0,0 +1,789 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "IT Business Solution - Laravel Test", + "description": "Laravel Test Documentation", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://laravel_test/api/", + "description": "Demo API Server" + } + ], + "paths": { + "/login": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Login", + "operationId": "authLogin", + "requestBody": { + "description": "Login", + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "type": "string", + "format": "text", + "example": "admin" + }, + "password": { + "type": "string", + "format": "password", + "example": "admin" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/project": { + "get": { + "tags": [ + "Projects" + ], + "summary": "Projects", + "operationId": "getProjects", + "parameters": [ + { + "name": "q", + "in": "query", + "description": "Search By Name", + "required": false, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + }, + { + "name": "pageIndex", + "in": "query", + "description": "Page Index (default 0)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + }, + "example": "" + }, + { + "name": "pageSize", + "in": "query", + "description": "Page Size (default 3)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + }, + "example": "" + }, + { + "name": "sortBy", + "in": "query", + "description": "Sort By (default by name)", + "required": false, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + }, + { + "name": "sortDirection", + "in": "query", + "description": "Sort Direction (default ASC)", + "required": false, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "post": { + "tags": [ + "Projects" + ], + "summary": "Create Projects", + "operationId": "storeProjects", + "requestBody": { + "description": "Create Project", + "required": true, + "content": { + "application/json": { + "schema": {}, + "example": { + "name": "Second Project" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/v1/project/{id}": { + "get": { + "tags": [ + "Projects" + ], + "summary": "Show Projects", + "operationId": "showProjects", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "delete": { + "tags": [ + "Projects" + ], + "summary": "Delete Project By Id", + "operationId": "deleteProjectId", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "patch": { + "tags": [ + "Projects" + ], + "summary": "Update Projects", + "operationId": "updateProjects", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "requestBody": { + "description": "Update Project", + "required": true, + "content": { + "application/json": { + "schema": {}, + "example": { + "name": "Update Project" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/v1/task": { + "get": { + "tags": [ + "Tasks" + ], + "summary": "Tasks", + "operationId": "getTasks", + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "post": { + "tags": [ + "Tasks" + ], + "summary": "Create Task", + "operationId": "storeTask", + "requestBody": { + "description": "Create Task", + "required": true, + "content": { + "application/json": { + "schema": {}, + "example": { + "title": "First Task", + "description": "This is First Task", + "project_id": "", + "user_id": "" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/v1/task/{id}": { + "get": { + "tags": [ + "Tasks" + ], + "summary": "Show Task", + "operationId": "showTask", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "delete": { + "tags": [ + "Tasks" + ], + "summary": "Delete Task By Id", + "operationId": "deleteTaskId", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "patch": { + "tags": [ + "Tasks" + ], + "summary": "Update Task", + "operationId": "updateTask", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "requestBody": { + "description": "Update Task", + "required": true, + "content": { + "application/json": { + "schema": {}, + "example": { + "title": "First Task", + "description": "This is First Task", + "status_id": 1, + "project_id": "", + "user_id": "" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/v1/user": { + "get": { + "tags": [ + "Users" + ], + "summary": "User", + "operationId": "getUsers", + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "post": { + "tags": [ + "Users" + ], + "summary": "Create Users", + "operationId": "createUser", + "requestBody": { + "description": "Create Users", + "required": true, + "content": { + "application/json": { + "schema": {}, + "example": { + "data": [ + { + "name": "User 1", + "username": "user1", + "password": "user1pwd", + "role_id": 1 + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/v1/user/{id}": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get User By Id", + "operationId": "getUsersId", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "delete": { + "tags": [ + "Users" + ], + "summary": "Delete User By Id", + "operationId": "deleteUserId", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "patch": { + "tags": [ + "Users" + ], + "summary": "Edit User ", + "operationId": "editUser", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id", + "required": true, + "schema": { + "type": "string", + "format": "text" + }, + "example": "1" + } + ], + "requestBody": { + "description": "Update User", + "required": true, + "content": { + "application/json": { + "schema": {}, + "example": { + "name": "User 1", + "username": "user1", + "password": "user1pwd", + "role_id": 1 + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Bad Request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error" + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header", + "bearerFormat": "JWT" + } + } + } +} \ No newline at end of file diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 4ae02bc..a330e7a 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -14,8 +14,12 @@ class ExampleTest extends TestCase */ public function test_example() { - $response = $this->get('/'); + $this->artisan('db:wipe'); + $this->artisan('migrate'); + $this->artisan('db:seed'); + + $response = $this->get('api'); $response->assertStatus(200); } } diff --git a/tests/Feature/ProjectApiTest.php b/tests/Feature/ProjectApiTest.php new file mode 100644 index 0000000..0eb421a --- /dev/null +++ b/tests/Feature/ProjectApiTest.php @@ -0,0 +1,30 @@ +getToken('product'); + + # Set Data + $projectData = [ + "name" => "Test Unit Project", + ]; + + # Post Data + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->post('/api/v1/project', $projectData); + + # Check Response + $response->assertStatus(201); + } +} diff --git a/tests/Feature/UserApiTest.php b/tests/Feature/UserApiTest.php new file mode 100644 index 0000000..0844cc3 --- /dev/null +++ b/tests/Feature/UserApiTest.php @@ -0,0 +1,42 @@ +getToken('admin'); + + # Set Data + $userData['data'] = [ + [ + "name" => "User Testing", + "username" => "testunit1", + "password"=> "123456", + "role_id"=> 1 + ],[ + "name" => "User Testing 2", + "username" => "testunit2", + "password"=> "123456", + "role_id"=> 1 + ] + ]; + + // # Post Data + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->post('/api/v1/user', $userData); + + # Check Response + $response->assertStatus(201); + + } +} diff --git a/tests/Feature/UserTaskApiTest.php b/tests/Feature/UserTaskApiTest.php new file mode 100644 index 0000000..4e49cd7 --- /dev/null +++ b/tests/Feature/UserTaskApiTest.php @@ -0,0 +1,76 @@ +getToken('admin'); + + # Get Users Data + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$tokenAdmin, + ])->get('/api/v1/user'); + + $getUsers = json_decode($response->content()); + + # Collect 2 users + \Log::info('api user: '.json_encode($getUsers->data)); + + $userList = []; + foreach ($getUsers->data as $key => $value) { + if ($value->role_id == 1) { + $userList[] = $value->id; + } + if (count($userList) == 2) { + break; + } + } + \Log::info('Users: '.json_encode($userList)); + + # Get Project Id + $responseProject = $this->withHeaders([ + 'Authorization' => 'Bearer '.$tokenAdmin, + ])->getJson('/api/v1/project',["q" => "Test Unit Project"]); + + $getProject = json_decode($responseProject->content()); + $projectId = $getProject->data[0]->id; + \Log::info('Project id: '.$projectId); + + # Create Task + if ($userList) { + # Get Token User + $tokenProduct = $this->getToken('product'); + + # Set Data + $projectData = [ + [ + "title" => "First Task", + "description" => "This is First Task", + "project_id" => $projectId + ],[ + "title" => "Second Task", + "description" => "This is Second Task", + "project_id" => $projectId + ] + ]; + + # Post Data + foreach ($userList as $key => $value) { + $projectData[$key]['user_id'] = $value; + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$tokenProduct, + ])->post('/api/v1/task', $projectData[$key]); + } + # Check Response + $response->assertStatus(201); + } + } +} diff --git a/tests/Feature/UserTaskStatusApiTest.php b/tests/Feature/UserTaskStatusApiTest.php new file mode 100644 index 0000000..ffc1340 --- /dev/null +++ b/tests/Feature/UserTaskStatusApiTest.php @@ -0,0 +1,39 @@ +getToken('member'); + + # Get Task Datas + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$tokenAdmin, + ])->get('/api/v1/task'); + + $getTasks = json_decode($response->content()); + + foreach ($getTasks->data as $key => $value) { + $postData = [ + "title" => $value->title, + "description" => $value->description, + "status_id" => 2 + ]; + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$tokenAdmin, + ])->patch('/api/v1/task/'.$value->id,$postData); + + \Log::info($response->content()); + } + $response->assertStatus(200); + } + + +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a..51bde14 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,4 +7,29 @@ abstract class TestCase extends BaseTestCase { use CreatesApplication; + + protected function getToken($role = '') { + $loginApiLink = '/api/login'; + $return = array(); + if ($role === 'admin') { + $return = $this->post($loginApiLink, [ + "username" => "admin", + "password" => "123456", + ]); + } + if ($role === 'product') { + $return = $this->post($loginApiLink, [ + "username" => "product", + "password" => "123456", + ]); + } + if ($role === 'member') { + $return = $this->post($loginApiLink, [ + "username" => "member", + "password" => "123456", + ]); + } + + return json_decode($return->content())->access_token; + } }