diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b39a2272..6de188ee 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -23,6 +23,10 @@ security: pattern: ^\/api\/ security: false + rider_api: + pattern: ^\/rapi\/ + security: false + main: form_login: login_path: login diff --git a/config/routes/rider_api.yaml b/config/routes/rider_api.yaml new file mode 100644 index 00000000..4cf98d93 --- /dev/null +++ b/config/routes/rider_api.yaml @@ -0,0 +1,41 @@ +# rider app api + +rapi_register: + path: /rapi/register + controller: App\Controller\RAPIController::register + methods: [POST] + +rapi_login: + path: /rapi/login + controller: App\Controller\RAPIController::login + methods: [POST] + +rapi_logout: + path: /rapi/logout + controller: App\Controller\RAPIController::logout + methods: [POST] + +rapi_jo_get: + path: /rapi/joborder + controller: App\Controller\RAPIController::getJobOrder + methods: [GET] + +rapi_jo_accept: + path: /rapi/accept + controller: App\Controller\RAPIController::acceptJobOrder + methods: [POST] + +rapi_jo_cancel: + path: /rapi/cancel + controller: App\Controller\RAPIController::cancelJobOrder + methods: [POST] + +rapi_arrive: + path: /rapi/arrive + controller: App\Controller\RAPIController::arrive + methods: [POST] + +rapi_payment: + path: /rapi/payment + controller: App\Controller\RAPIController::payment + methods: [POST] diff --git a/src/Controller/RAPIController.php b/src/Controller/RAPIController.php new file mode 100644 index 00000000..4dab01ff --- /dev/null +++ b/src/Controller/RAPIController.php @@ -0,0 +1,453 @@ +session = null; + } + + protected function checkMissingParameters(Request $req, $params = []) + { + $missing = []; + + // check if parameters are there + foreach ($params as $param) + { + if ($req->getMethod() == 'GET') + { + $check = $req->query->get($param); + if (empty($check)) + $missing[] = $param; + } + else if ($req->getMethod() == 'POST') + { + $check = $req->request->get($param); + if (empty($check)) + $missing[] = $param; + } + else + return $params; + } + + return $missing; + } + + // TODO: type hint entity manager + protected function checkAPIKey($em, $api_key) + { + // find the api key (session id) + $session = $em->getRepository(RiderSession::class)->find($api_key); + if ($session == null) + return null; + + return $session; + } + + protected function checkParamsAndKey(Request $req, $em, $params) + { + // returns APIResult object + $res = new APIResult(); + + // check for api_key in query string + $api_key = $req->query->get('api_key'); + if (empty($api_key)) + { + $res->setError(true) + ->setErrorMessage('Missing API key'); + return $res; + } + + // check missing parameters + $missing = $this->checkMissingParameters($req, $params); + if (count($missing) > 0) + { + $miss_string = implode(', ', $missing); + $res->setError(true) + ->setErrorMessage('Missing parameter(s): ' . $miss_string); + return $res; + } + + // check api key + $sess = $this->checkAPIKey($em, $req->query->get('api_key')); + if ($sess == null) + { + $res->setError(true) + ->setErrorMessage('Invalid API Key'); + return $res; + } + + // store session + $this->session = $sess; + + return $res; + } + + public function register(Request $req) + { + $res = new APIResult(); + + // confirm parameters + $required_params = [ + 'phone_number', + 'device_push_id' + ]; + + $missing = $this->checkMissingParameters($req, $required_params); + if (count($missing) > 0) + { + $params = implode(', ', $missing); + $res->setError(true) + ->setErrorMessage('Missing parameter(s): ' . $params); + return $res->getReturnResponse(); + } + + $em = $this->getDoctrine()->getManager(); + + // retry until we get a unique id + while (true) + { + try + { + // instantiate session + $sess = new RiderSession(); + $sess->setPhoneNumber($req->request->get('phone_number')) + ->setDevicePushID($req->request->get('device_push_id')); + + // reopen in case we get an exception + if (!$em->isOpen()) + { + $em = $em->create( + $em->getConnection(), + $em->getConfiguration() + ); + } + + // save + $em->persist($sess); + $em->flush(); + } + catch (DBALException $e) + { + error_log($e->getMessage()); + // delay one second and try again + sleep(1); + continue; + } + + break; + } + + // return data + $data = [ + 'session_id' => $sess->getID() + ]; + $res->setData($data); + + + // response + return $res->getReturnResponse(); + } + + public function login(Request $req, EncoderFactoryInterface $ef) + { + $required_params = [ + 'user', + 'pass', + ]; + $em = $this->getDoctrine()->getManager(); + $res = $this->checkParamsAndKey($req, $em, $required_params); + if ($res->isError()) + return $res->getReturnResponse(); + + // check if session has a rider already + if ($this->session->hasRider()) + { + $res->setError(true) + ->setErrorMessage('Another rider is already logged in. Please logout first.'); + return $res->getReturnResponse(); + } + + // look for rider with username + $rider = $em->getRepository(Rider::class)->findOneBy(['username' => $req->request->get('user')]); + if ($rider == null) + { + $res->setError(true) + ->setErrorMessage('Invalid username or password.'); + return $res->getReturnResponse(); + } + + // check if rider password is correct + $encoder = $ef->getEncoder(new User()); + if (!$encoder->isPasswordValid($rider->getPassword(), $req->request->get('pass'), '')) + { + $res->setError(true) + ->setErrorMessage('Invalid username or password.'); + return $res->getReturnResponse(); + } + + // assign rider to session + $this->session->setRider($rider); + + // TODO: log rider logging in + + $em->flush(); + + return $res->getReturnResponse(); + } + + public function logout(Request $req) + { + $required_params = []; + $em = $this->getDoctrine()->getManager(); + $res = $this->checkParamsAndKey($req, $em, $required_params); + if ($res->isError()) + return $res->getReturnResponse(); + + // remove rider from session + $this->session->setRider(null); + + // TODO: log rider logging out + + $em->flush(); + + return $res->getReturnResponse(); + } + + public function getJobOrder(Request $req) + { + // get the job order of the rider assigned to this session + $required_params = []; + $em = $this->getDoctrine()->getManager(); + $res = $this->checkParamsAndKey($req, $em, $required_params); + if ($res->isError()) + return $res->getReturnResponse(); + + // are we logged in? + if (!$this->session->hasRider()) + { + $res->setError(true) + ->setErrorMessage('No logged in rider.'); + return $res->getReturnResponse(); + } + + $rider = $this->session->getRider(); + + // do we have a job order? + $jo = $rider->getActiveJobOrder(); + if ($jo == null) + { + $data = [ + 'job_order' => null + ]; + } + else + { + $coord = $jo->getCoordinates(); + $cust = $jo->getCustomer(); + $cv = $jo->getCustomerVehicle(); + $v = $cv->getVehicle(); + $inv = $jo->getInvoice(); + + // invoice items + $inv_items = []; + foreach ($inv->getItems() as $item) + { + $inv_items[] = [ + 'id' => $item->getID(), + 'title' => $item->getTitle(), + 'qty' => $item->getQuantity(), + 'price' => $item->getPrice(), + ]; + } + + $data = [ + 'job_order' => [ + 'id' => $jo->getID(), + 'service_type' => $jo->getServiceType(), + 'date_schedule' => $jo->getDateSchedule()->format('Ymd'), + 'longitude' => $coord->getLongitude(), + 'latitude' => $coord->getLatitude(), + 'status' => $jo->getStatus(), + 'customer' => [ + 'title' => $cust->getTitle(), + 'first_name' => $cust->getFirstName(), + 'last_name' => $cust->getLastName(), + 'phone_mobile' => $cust->getPhoneMobile(), + ], + 'vehicle' => [ + 'manufacturer' => $v->getManufacturer()->getName(), + 'make' => $v->getMake(), + 'model' => $cv->getModelYear(), + 'plate_number' => $cv->getPlateNumber(), + 'color' => $cv->getColor(), + ], + 'delivery_instructions' => $jo->getDeliveryInstructions(), + 'delivery_address' => $jo->getDeliveryAddress(), + 'landmark' => $jo->getLandmark(), + 'invoice' => [ + 'discount' => $inv->getDiscount(), + 'trade_in' => $inv->getTradeIn(), + 'total_price' => $inv->getTotalPrice(), + 'vat' => $inv->getVat(), + 'items' => $inv_items, + ], + 'mode_of_payment' => $jo->getModeOfPayment(), + + + ] + ]; + } + + $res->setData($data); + + return $res->getReturnResponse(); + } + + protected function checkJO(Request $req, $required_params) + { + // set jo status to in transit + $em = $this->getDoctrine()->getManager(); + $res = $this->checkParamsAndKey($req, $em, $required_params); + if ($res->isError()) + return $res; + + // are we logged in? + if (!$this->session->hasRider()) + { + $res->setError(true) + ->setErrorMessage('No logged in rider.'); + return $res; + } + + $rider = $this->session->getRider(); + + // check if we have an active JO + $jo = $rider->getActiveJobOrder(); + if ($jo == null) + { + $res->setError(true) + ->setErrorMessage('No active job order.'); + return $res; + } + + // check if the jo_id sent is the same as our active jo + if ($req->request->get('jo_id') != $jo->getID()) + { + $res->setError(true) + ->setErrorMessage('Job order selected is not active job order.'); + return $res; + } + + return $res; + } + + public function acceptJobOrder(Request $req) + { + $required_params = ['jo_id']; + $res = $this->checkJO($req, $required_params); + if ($res->isError()) + return $res->getReturnResponse(); + + // TODO: refactor this into a jo handler class, so we don't have to repeat for control center + + // set jo status to in transit + $jo->setStatus(JOStatus::IN_TRANSIT); + + // TODO: send mqtt event + + // TODO: add event + + return $res->getReturnResponse(); + } + + public function cancelJobOrder(Request $req) + { + $required_params = ['jo_id']; + $res = $this->checkJO($req, $required_params); + if ($res->isError()) + return $res->getReturnResponse(); + + // TODO: refactor this into a jo handler class, so we don't have to repeat for control center + + // set jo status to cancelled + $jo->setStatus(JOStatus::CANCELLED); + + // TODO: send mqtt event + + // TODO: add event + + return $res->getReturnResponse(); + } + + public function arrive(Request $req) + { + $required_params = ['jo_id']; + $res = $this->checkJO($req, $required_params); + if ($res->isError()) + return $res->getReturnResponse(); + + // TODO: refactor this into a jo handler class, so we don't have to repeat for control center + + // set jo status to in progress + $jo->setStatus(JOStatus::IN_PROGRESS); + + // TODO: send mqtt event + + // TODO: add event + + return $res->getReturnResponse(); + } + + public function payment(Request $req) + { + // set invoice to paid + + // set jo status to fulfilled + } +} diff --git a/src/Controller/RiderController.php b/src/Controller/RiderController.php index 84e2f21d..16b9bb8c 100644 --- a/src/Controller/RiderController.php +++ b/src/Controller/RiderController.php @@ -7,6 +7,7 @@ use App\Ramcar\DayOfWeek; use App\Entity\Rider; use App\Entity\RiderSchedule; use App\Entity\Hub; +use App\Entity\User; use App\Service\FileUploader; use Doctrine\ORM\Query; @@ -14,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use DateTime; @@ -154,7 +156,8 @@ class RiderController extends BaseController ->setContactNumber($req->request->get('contact_no')) ->setPlateNumber($req->request->get('plate_number')) ->setImageFile($req->request->get('image_file')) - ->setActive($req->request->get('flag_active') ? true : false); + ->setActive($req->request->get('flag_active') ? true : false) + ->setUsername($req->request->get('username')); } public function addSubmit(Request $req, EncoderFactoryInterface $ef, ValidatorInterface $validator) @@ -176,6 +179,24 @@ class RiderController extends BaseController // initialize error list $error_array = []; + // get password inputs + $password = $req->request->get('password'); + $confirm_password = $req->request->get('confirm_password'); + + // custom validation for password fields + if (!$password) { + $error_array['password'] = 'This value should not be blank.'; + } else if ($password != $confirm_password) { + $error_array['confirm_password'] = 'Passwords do not match.'; + } else { + // encode password + $enc = $ef->getEncoder(new User()); + $encoded_password = $enc->encodePassword($req->request->get('password'), ''); + + // set password + $obj->setPassword($encoded_password); + } + // custom validation for associations $hub_id = $req->request->get('hub'); @@ -303,6 +324,24 @@ class RiderController extends BaseController // initialize error list $error_array = []; + // get password inputs + $password = $req->request->get('password'); + $confirm_password = $req->request->get('confirm_password'); + + // custom validation for password fields + if ($password || $confirm_password) { + if ($password != $confirm_password) { + $error_array['confirm_password'] = 'Passwords do not match.'; + } else { + // encode password + $enc = $ef->getEncoder(new User()); + $encoded_password = $enc->encodePassword($req->request->get('password'), ''); + + // set password + $obj->setPassword($encoded_password); + } + } + // custom validation for associations $hub_id = $req->request->get('hub'); diff --git a/src/Entity/Rider.php b/src/Entity/Rider.php index d637bc99..02bca560 100644 --- a/src/Entity/Rider.php +++ b/src/Entity/Rider.php @@ -8,6 +8,8 @@ use Symfony\Component\Validator\Constraints as Assert; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Criteria; +use App\Ramcar\JOStatus; + /** * @ORM\Entity * @ORM\Table(name="rider") @@ -92,6 +94,18 @@ class Rider */ protected $flag_active; + // username for rider api + /** + * @ORM\Column(type="string", length=80, unique=true, nullable=true) + */ + protected $username; + + // password for rider api + /** + * @ORM\Column(type="string", length=64) + */ + protected $password; + public function __construct() { $this->job_orders = new ArrayCollection(); @@ -99,6 +113,8 @@ class Rider $this->curr_rating = 0; $this->flag_available = true; $this->flag_active = true; + $this->username = null; + $this->password = ''; } public function getID() @@ -253,4 +269,42 @@ class Rider { return $this->flag_active; } + + public function setUsername($username) + { + $this->username = $username; + return $this; + } + + public function getUsername() + { + return $this->username; + } + + public function setPassword($pass) + { + // they have to pass the encoded password + $this->password = $pass; + return $this; + } + + public function getPassword() + { + return $this->password; + } + + public function getActiveJobOrder() + { + $active_status = [ + JOStatus::ASSIGNED, + JOStatus::IN_TRANSIT, + JOStatus::IN_PROGRESS, + ]; + + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->in('status', $active_status)) + ->getFirstResult(1); + + return $this->job_orders->matching($criteria)[0]; + } } diff --git a/src/Entity/RiderSession.php b/src/Entity/RiderSession.php new file mode 100644 index 00000000..4c879cd7 --- /dev/null +++ b/src/Entity/RiderSession.php @@ -0,0 +1,117 @@ +id = $this->generateKeyID(); + $this->rider = null; + $this->is_active = false; + } + + public function generateKeyID() + { + // use uniqid for now, since primary key dupes will trigger exceptions + return uniqid(); + } + + public function getID() + { + return $this->id; + } + + public function setDevicePushID($id) + { + $this->device_push_id = $id; + return $this; + } + + public function getDevicePushID() + { + return $this->device_push_id; + } + + public function setRider(Rider $rider = null) + { + $this->rider = $rider; + return $this; + } + + public function getRider() + { + return $this->rider; + } + + public function setPhoneNumber($num) + { + $this->phone_number = $num; + return $this; + } + + public function getPhoneNumber() + { + return $this->phone_number; + } + + public function setActive($flag = true) + { + $this->is_active = $flag; + return $this; + } + + public function isActive() + { + return $this->is_active; + } + + public function hasRider() + { + if ($this->rider == null) + return false; + + return true; + } +} diff --git a/templates/rider/form.html.twig b/templates/rider/form.html.twig index c6501ca9..c08c250c 100644 --- a/templates/rider/form.html.twig +++ b/templates/rider/form.html.twig @@ -35,6 +35,36 @@