== Implementation of user scenarios in the prototype In the technical prototype of the application implemented so far, the following scenarios have been implemented: ||= ID =||||= !UseCase =|| ||1||||'''[wiki:SearchRoutesUseCase Search Routes]'''|| ||2||||'''[wiki:OrganizeTripUseCase Organize Trip]'''|| ||3||||'''[wiki:ManageTripUseCase Manage Trip]'''|| ||4||||'''[wiki:ViewMyTripsUseCase View My Trips]'''|| ||5||||'''[wiki:LoginUseCase Login]'''|| ||6||||'''[wiki:RegisterUseCase Register]'''|| == **ID 1** - Search Routes The `/search-routes` endpoint provides users with the ability to search for available transport routes from different organizers. It supports both **viewing all routes** by default and **filtering routes** based on user input (departure and arrival locations). The request is handled by `UserRouteController`, which interacts with `RouteService` to retrieve the relevant data. Here is an example of a search query: -`/search-routes?from=sk&to=ohrid` === {{{ @RequestMapping("/search-routes") public class UserRouteController { private final RouteService routeService; public UserRouteController(RouteService routeService) { this.routeService = routeService; } @GetMapping public String findRoutesByFromAndTo(@RequestParam(required = false) String from, @RequestParam(required = false) String to, Model model) { List filteredRoutes = ((from == null || from.isBlank()) && (to == null || to.isBlank())) ? routeService.findAll() : routeService.findRouteByFromAndToDest(from, to); model.addAttribute(filteredRoutes.isEmpty() ? "noRoutesMessage" : "routes", filteredRoutes.isEmpty() ? "No routes found for your search." : filteredRoutes); model.addAttribute("display", "user/search-routes"); return "master"; } } }}} === ==== Controller Details: - **URL Mapping:** `/search-routes` - **Method:** `GET` - **Functionality:** - Displays **all routes** when the page is accessed without search criteria. - Filters results based on the user's input (`from` and `to`). - **View:** Uses the `master` template and dynamically embeds `user/search-routes`. ==== Breakdown: - When users first visit `/search-routes`, the controller calls `routeService.findAll()` to display all available routes. - When users provide search criteria (`from` and/or `to`), it filters results using `routeService.findRouteByFromAndToDest(from, to)`. - If no matching routes are found, a **"No routes found for your search."** message is displayed instead of an empty table. - The retrieved data is added to the `Model`, ensuring the front-end can render the appropriate content. === ==== Result of `/search-routes` [[Image(search-routes.png, 100%)]] === ==== Result of `/search-routes?from=sk&to=ohrid` [[Image(search-routes-filter.png, 100%)]] === == **ID 2** - Organize Trip The `/routes` endpoint provides transport organizers with the ability to manage their routes and organize trips. It supports **viewing authorized routes**, **viewing trips for a specific route**, and **adding new trips**. The request is handled by `CompanyRouteController` and `CompanyTripController`, which interact with `CompanyRouteService` and `CompanyTripService` to manage routes and trips. Here is an example of the flow to organize a trip: - View routes: `/routes/company` - View trips for a specific route: `/routes/company/view-trips/{routeId}` - Add a new trip: `/routes/company/view-trips/{routeId}/add-trip` === {{{ @RequestMapping("/routes") public class CompanyRouteController { private final CompanyRouteService companyRouteService; public CompanyRouteController(CompanyRouteService companyRouteService) { this.companyRouteService = companyRouteService; } @GetMapping("/company") public String routes(Model model) { model.addAttribute("companyRoutes", companyRouteService.getAuthorizedRoutes()); model.addAttribute("display", "/company/company-route"); return "master"; } } }}} === {{{ @RequestMapping("/routes/company/view-trips/{routeId}") public class CompanyTripController { private final CompanyTripService companyTripService; private final RouteService routeService; private final LocationService locationService; public CompanyTripController(CompanyTripService companyTripService, RouteService routeService, LocationService locationService) { this.companyTripService = companyTripService; this.routeService = routeService; this.locationService = locationService; } @GetMapping public String routeTrips(@PathVariable Integer routeId, Model model) { Route route = routeService.findById(routeId); model.addAttribute("trips", companyTripService.getAuthorizedTripsByRoute(routeId)); model.addAttribute("routeId", routeId); model.addAttribute("locations", locationService.findAll()); model.addAttribute("routeSource", route.getSource()); model.addAttribute("routeDestination", route.getDestination()); model.addAttribute("display", "/company/company-view-trip"); return "master"; } @PostMapping("/add-trip") public String addNewTrip(@PathVariable Integer routeId, @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam("freeSeats") int freeSeats, @RequestParam("locations") List locationIds, @RequestParam("etas") @DateTimeFormat(pattern = "HH:mm") List etas, RedirectAttributes redirectAttributes) { try { Route route = routeService.findById(routeId); companyTripService.createTrip(route, date, freeSeats, locationIds, etas); redirectAttributes.addFlashAttribute("message", "Trip created successfully!"); } catch (IllegalArgumentException | SecurityException e) { redirectAttributes.addFlashAttribute("error", e.getMessage()); } return "redirect:/routes/company/view-trips/" + routeId; } } }}} === ==== Controller Details: - **URL Mapping:** `/routes` - **Method:** `GET`, `POST` - **Functionality:** - **GET `/routes/company`**: Displays **authorized routes** for the transport organizer. - **GET `/routes/company/view-trips/{routeId}`**: Displays all **authorized trips** for the selected route. - **POST `/routes/company/view-trips/{routeId}/add-trip`**: Allows the transport organizer to **add a new trip** to an existing route. - **View:** Uses the `master` template and dynamically embeds `/company/company-route` and `/company/company-view-trip`. ==== Breakdown: - When the transport organizer accesses `/routes/company`, the controller retrieves all authorized routes using `companyRouteService.getAuthorizedRoutes()` and displays them. - When the transport organizer accesses `/routes/company/view-trips/{routeId}`, the controller retrieves all authorized trips for the specified route using `companyTripService.getAuthorizedTripsByRoute(routeId)` and displays them along with the locations. {{{ /** * Returns every trip for specific route if user is authorized. * * @param routeId the routeId for getting authorized trips * @return all trips for the route || SecurityException("Unauthorized to access these trips.") */ public List getAuthorizedTripsByRoute(Integer routeId) { Integer transportOrganizerId = authorizationService.getAuthenticatedTransportOrganizerId(); List trips = tripService.findAllByPredicate(TripSpecification.tripsByRoute(routeId)); if (!trips.isEmpty() && !trips.get(0).getTranOrg().getTranOrgId().equals(transportOrganizerId)) { throw new SecurityException("Unauthorized to access these trips."); } return trips; } }}} - The transport organizer can add a new trip by submitting the form with details like date, free seats, locations, and estimated times of arrival (ETAs). === ==== Result of `/routes/company` [[Image(company-routes-view.png, 100%)]] === ==== Result of `/routes/company/view-trips/{routeId}` [[Image(company-trips-view.png, 100%)]] === === Result of `/routes/company/view-trips/{routeId}/add-trip` [[Image(add-modalv1.png, 100%)]] [[Image(add-trip-success.png, 100%)]] === == **ID 3** - Manage Trip The `/routes` endpoint allows transport organizers to manage their trips. It includes the ability to **view, add, edit, and delete trips** for their authorized routes. Here is an example of the flow to manage trips: - View routes: `/routes/company` - View trips for a specific route: `/routes/company/view-trips/{routeId}` - Add a new trip: `/routes/company/view-trips/{routeId}/add-trip` - Edit an existing trip: `/routes/company/view-trips/{routeId}/edit-trip/{tripId}` - Delete a trip: `/routes/company/view-trips/{routeId}/delete-trip/{tripId}` === {{{ @RequestMapping("/routes") public class CompanyRouteController { private final CompanyRouteService companyRouteService; public CompanyRouteController(CompanyRouteService companyRouteService) { this.companyRouteService = companyRouteService; } @GetMapping("/company") public String routes(Model model) { model.addAttribute("companyRoutes", companyRouteService.getAuthorizedRoutes()); model.addAttribute("display", "/company/company-route"); return "master"; } } }}} === {{{ @RequestMapping("/routes/company/view-trips/{routeId}") public class CompanyTripController { private final CompanyTripService companyTripService; private final RouteService routeService; private final LocationService locationService; public CompanyTripController(CompanyTripService companyTripService, RouteService routeService, LocationService locationService) { this.companyTripService = companyTripService; this.routeService = routeService; this.locationService = locationService; } @GetMapping public String routeTrips(@PathVariable Integer routeId, Model model) { Route route = routeService.findById(routeId); model.addAttribute("trips", companyTripService.getAuthorizedTripsByRoute(routeId)); model.addAttribute("routeId", routeId); model.addAttribute("locations", locationService.findAll()); model.addAttribute("routeSource", route.getSource()); model.addAttribute("routeDestination", route.getDestination()); model.addAttribute("display", "/company/company-view-trip"); return "master"; } @PostMapping("/add-trip") public String addNewTrip(@PathVariable Integer routeId, @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam("freeSeats") int freeSeats, @RequestParam("locations") List locationIds, @RequestParam("etas") @DateTimeFormat(pattern = "HH:mm") List etas, RedirectAttributes redirectAttributes) { try { Route route = routeService.findById(routeId); companyTripService.createTrip(route, date, freeSeats, locationIds, etas); redirectAttributes.addFlashAttribute("message", "Trip created successfully!"); } catch (IllegalArgumentException | SecurityException e) { redirectAttributes.addFlashAttribute("error", e.getMessage()); } return "redirect:/routes/company/view-trips/" + routeId; } @PostMapping("/edit-trip/{tripId}") public String editTrip(@PathVariable Integer routeId, @PathVariable Integer tripId, @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam("freeSeats") int freeSeats, @RequestParam("locations") List locationIds, @RequestParam("etas") @DateTimeFormat(pattern = "HH:mm") List etas, RedirectAttributes redirectAttributes) { try { Route route = routeService.findById(routeId); companyTripService.updateTrip(route, tripId, date, freeSeats, locationIds, etas); redirectAttributes.addFlashAttribute("message", "Trip updated successfully!"); } catch (IllegalArgumentException | SecurityException e) { redirectAttributes.addFlashAttribute("error", e.getMessage()); } return "redirect:/routes/company/view-trips/" + routeId; } @PostMapping("/delete-trip/{tripId}") public String deleteTrip(@PathVariable Integer routeId, @PathVariable Integer tripId) { return companyTripService.deleteTripIfAuthorized(tripId) ? "redirect:/routes/company/view-trips/" + routeId : "redirect:/"; } } }}} === ==== Controller Details for Routes: - **URL Mapping:** `/routes/company` - **Method:** `GET` - **Functionality:** Displays all **authorized routes** for the transport organizer. - **View:** Uses the `master` template and dynamically embeds `/company/company-route`. ==== Breakdown: - Displays a list of all routes that the transport organizer is authorized to manage. - Retrieves authorized routes from the `CompanyRouteService` and populates them into the `Model`. - The view is displayed using `/company/company-route`. === ==== Controller Details for Trips: - **URL Mapping:** `/routes/company/view-trips/{routeId}` - **Method:** `GET`, `POST` - **Functionality:** - **GET `/routes/company/view-trips/{routeId}`**: Displays **all trips** for the selected route. - **POST `/routes/company/view-trips/{routeId}/add-trip`**: Allows the transport organizer to **add a new trip**. - **POST `/routes/company/view-trips/{routeId}/edit-trip/{tripId}`**: Allows the transport organizer to **edit an existing trip**. - **POST `/routes/company/view-trips/{routeId}/delete-trip/{tripId}`**: Allows the transport organizer to **delete a trip**. - **View:** Uses the `master` template and dynamically embeds `/company/company-view-trip`. ==== Breakdown: - **Viewing trips**: When the transport organizer accesses `/routes/company/view-trips/{routeId}`, the controller retrieves all trips for the specified route using `companyTripService.getAuthorizedTripsByRoute(routeId)`. - **Adding a new trip**: A form allows the transport organizer to specify the date, free seats, locations, and ETAs for the trip. The controller calls `companyTripService.createTrip()` to create a new trip. - **Editing a trip**: The transport organizer can modify trip details. The controller calls `companyTripService.updateTrip()` to update the trip. - **Deleting a trip**: The transport organizer can delete a trip. The controller calls `companyTripService.deleteTripIfAuthorized()` to remove the trip if the organizer is authorized. - **Viewing trip stops trip** The transport organizer can view and modify trip stops. The controller calls companyTripService.updateTrip() to update whatever the authorized transport organizer wants. === ==== Edit Trip: ===== **EXAMPLE: Free seats modified, changed time and deleted stop.** [[Image(edit-trip.png, 100%)]] [[Image(edited-trip.png, 100%)]] === ==== Delete Trip: [[Image(delete-trip.png, 100%)]] === ==== Trip Stops: [[Image(trip-stops.png, 100%)]] === == **ID 4** - View My Trips The `/trips/user` endpoint allows authenticated users to view the list of trips they have booked. It retrieves the trips associated with the currently logged-in user and displays them dynamically. The request is handled by `UserTripController`, which interacts with `TripService` and `AuthenticationService` to fetch user-specific trip data. === **Example URL for viewing booked trips:** - `/trips/user` === {{{ @RequestMapping("/trips") public class UserTripController { private final AuthenticationService authenticationService; private final TripService tripService; public UserTripController(AuthenticationService authenticationService, TripService tripService) { this.authenticationService = authenticationService; this.tripService = tripService; } @GetMapping("/user") public String myTripsPage(Model model) { Integer currentAccountId = authenticationService.getAuthenticatedUserId(); model.addAttribute("trips", tripService.findTripsBookedByAccount(currentAccountId)); model.addAttribute("display", "user/my-trips"); return "master"; } } }}} === ==== **Controller Details:** - **URL Mapping:** `/trips/user` - **Method:** GET - **Functionality:** - Retrieves the currently logged-in user’s booked trips using `tripService.findTripsBookedByAccount(currentAccountId)`. - Passes the retrieved trips to the model under the key `"trips"`. - Uses the `"master"` template and embeds `"user/my-trips"` dynamically for rendering. ==== **Breakdown:** - When a user accesses `/trips/user`, the controller first retrieves the authenticated user's ID using `authenticationService.getAuthenticatedUserId()`. - It then fetches all trips booked by the user from `tripService.findTripsBookedByAccount(currentAccountId)`. - The retrieved trips are added to the model, ensuring they can be displayed on the front end. === ==== **Result of /trips/user** [[Image(my-trips.png, 100%)]] === == **ID-5** - Login The `/login` endpoint provides users with authentication functionality through a custom authentication provider. It supports user authentication using email and password credentials, with proper security measures including password encryption and role-based authorization. === Authentication Flow The login process follows these steps: 1. User accesses the login page: `/login` 2. User submits credentials 3. System validates and authenticates the user 4. User is redirected based on authentication result === {{{ @Controller @RequestMapping("/login") public class LoginController { @GetMapping public String loginPage(Model model) { model.addAttribute("display", "login"); return "master"; } } }}} === ==== Controller Details: - **URL Mapping:** `/login` - **Method:** `GET` - **Functionality:** - Displays the login form when accessed - Handles authentication through Spring Security configuration - **View:** Uses the `master` template and dynamically embeds `login` view ==== Authentication Provider Implementation: The `CustomAuthenticationProvider` handles the core authentication logic: {{{ @Component public class CustomAuthenticationProvider implements AuthenticationProvider { private final AccountService accountService; private final PasswordEncoder passwordEncoder; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = authentication.getCredentials().toString(); if (username.isEmpty() || password.isEmpty()) { throw new BadCredentialsException("Please fill out all fields."); } try { UserDetails userDetails = accountService.findOneByPredicate( AccountSpecification.hasEmail(username)); if (!passwordEncoder.matches(password, userDetails.getPassword())) { throw new BadCredentialsException("Invalid username or password"); } return new UsernamePasswordAuthenticationToken( userDetails, userDetails.getPassword(), userDetails.getAuthorities() ); } catch (EntityNotFoundException exc) { throw new BadCredentialsException(exc.getMessage()); } } } }}} ==== Security Configuration: The security rules are defined in `SecurityConfig`: {{{ @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests( authorizeRequests -> authorizeRequests .requestMatchers("/", "/home", "/login", "/css/**", "/js/**", "/images/**", "/register", "/search-routes") .permitAll() .requestMatchers("/routes/company/**") .hasAnyRole("TRANSPORT_ORGANIZER", "DRIVER") .requestMatchers("/admin/**") .hasRole("ADMIN") .requestMatchers("/trips/user/**") .hasRole("USER") .anyRequest().authenticated() ) .formLogin(login -> login.loginPage("/login") .permitAll() .defaultSuccessUrl("/") .failureUrl("/login?error") ); return http.build(); } } }}} ==== Role Resolution: User roles are determined through the `RoleResolver` utility: {{{ public class RoleResolver { public static Collection resolveRoles(Account account) { List authorities = new ArrayList<>(); if (account.getAdmin() != null) { authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } if (account.getStudent() != null) { authorities.add(new SimpleGrantedAuthority("ROLE_STUDENT")); } if (account.getDriver() != null) { authorities.add(new SimpleGrantedAuthority("ROLE_DRIVER")); } if (account.getTransportOrganizer() != null) { authorities.add(new SimpleGrantedAuthority("ROLE_TRANSPORT_ORGANIZER")); } authorities.add(new SimpleGrantedAuthority("ROLE_USER")); return authorities; } } }}} === ==== Result of `/login` page [[Image(login.png, 100%)]] === ==== Result of failed login attempt [[Image(login-error.png, 100%)]] === == **ID-6** - Register The `/register` endpoint provides user registration functionality, allowing new users to create accounts in the system. It supports basic user registration with name, surname, email, and password validation. === Registration Flow The registration process follows these steps: 1. User accesses the registration page: `/register` 2. User fills out the registration form with required information 3. System validates the input and creates a new account 4. User is redirected to login page upon successful registration === {{{ @Controller @RequestMapping("/register") public class RegisterController { private final AccountService accountService; @GetMapping public String registerPage(@RequestParam(required = false) String error, Model model) { if (error != null) { model.addAttribute("errorMessage", error); } model.addAttribute("display", "register"); return "master"; } @PostMapping private String registerUser(@RequestParam String name, @RequestParam String surname, @RequestParam String email, @RequestParam String password, @RequestParam String confirmPassword) { try { accountService.save(email, name, surname, password, confirmPassword); } catch (Exception e) { return String.format("redirect:/register?error=%s", e.getMessage()); } return "redirect:/login"; } } }}} === ==== Controller Details: - **URL Mapping:** `/register` - **Methods:** - `GET`: Displays registration form - `POST`: Processes registration request - **Functionality:** - Displays registration form with error handling - Processes user registration with field validation - Handles registration errors and displays appropriate messages - Redirects to login page upon successful registration - **View:** Uses the `master` template and dynamically embeds `register` view ==== Registration Fields: - **Required Information:** - Name - Surname - Email (used as username for login) - Password - Password Confirmation === ==== Result of `/register` page [[Image(register.png, 100%)]] ===