wiki:UseCaseImplementationsFinal

Version 4 (modified by 211012, 5 months ago) ( diff )

--

Финална имплементација на кориснички сценарија

До оваа фаза беа имплементирани сите предвидени кориснички сценарија, односно:

ID Use Case
1 Се најавува на системот
2 Се регистрира на системот
3 Листа сместување
4 Листа превоз
5 Листа ресторани
6 Резервира сместување
7 Резервира превоз
8 Резервира ресторани
9 Откажува услуга
10 Пријавува сместување
11 Пријавува превоз
12 Пријавува ресторан
13 Менаџира резервации
14 Регистрира нов бизнис
15 Менува понуда за сместување
16 Менува понуда за превоз
17 Менува понуда за ресторан
15 Управува со профили
16 Верифицира профили
17 Внесува оценка

Во нив нема некакви поголеми промени, освен во листањето на сместувања кои ќе бидат наведени во продолжение.

Дополнително се имплементирани следните сценарија по актери:

Ненајавен корисник

ID Use Case
18 Се најавува на системот преку надворешен систем
21 Поврзува профил

Систем

ID Use Case
19 Испраќа известување до администратори
20 Испраќа известување до корисници на услуги

Имплементација

Се најавува на системот преку надворешен систем

Во последната верзија, со имплементација на OAuth2 клиентот, системот овозможува најава преку користење на профил од Google, Facebook или GitHub. Конфигурацијата е додадена на веќе постоечката за Spring Security, со тоа што дополнително е имплементиран CustomOAuth2AuthenticationService сервис за справување со барањата за регистрација и најава со профил од некој од претходно споментатите провајдери.

...
                                        .and()
                                        .oauth2Login()
                                        .loginPage("/login")
                                        .permitAll()
                                        .userInfoEndpoint(x -> x.userService(customOAuth2UserDetailService))
                                        .successHandler(oAuth2SuccessHandler)
                                        .failureHandler(oAuth2FailureHandler)
...

За правилно справување со различните формати на одговор кои секоја од страниците го враќа, преку имплементација на шаблонот за развој на софтвер Factory, имплементиравме фабрика за објекти од типот OAuth2UserDetails, при што во зависност од сценариото соодветно се преземаат потребните атрибути за да може корисникот да идентификува и/или регистрира во нашата база.

public abstract class OAuth2UserDetails {
    protected Map<String, Object> attributes;

    public OAuth2UserDetails(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    public abstract String getName();
    public abstract String getEmail();
}
public class OAuth2GoogleUser extends OAuth2UserDetails{
    public OAuth2GoogleUser(Map<String, Object> attributes) {
        super(attributes);
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }
}
public class OAuth2GitHubUser extends OAuth2UserDetails{
    public OAuth2GitHubUser(Map<String, Object> attributes) {
        super(attributes);
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("login");
    }
}
public class OAuth2FacebookUser extends OAuth2UserDetails{
    public OAuth2FacebookUser(Map<String, Object> attributes) {
        super(attributes);
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }
}
public class OAuth2UserDetailsFactory {
    public static OAuth2UserDetails createOAuth2UserDetails (String registrationId, Map<String, Object> attributes)
    {
        if(registrationId.equals(Providers.google.name()))
        {
            return new OAuth2GoogleUser(attributes);
        }
        else if(registrationId.equals(Providers.facebook.name()))
        {
            return new OAuth2FacebookUser(attributes);
        }
        else if(registrationId.equals(Providers.github.name()))
        {
            return new OAuth2GitHubUser(attributes);
        }
        else
        {
            throw  new RuntimeException("Login with this provider is not supported!");
        }
    }
}

Објектот од ваков тип соодветно се креира во сервисот за автентикација преку повик на статичкиот метод.

@Service
public class CustomOAuth2UserDetailService extends DefaultOAuth2UserService {

    private final UsersDao usersDao;

    public CustomOAuth2UserDetailService(UsersDao usersDao) {
        this.usersDao = usersDao;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        try
        {
            return checkOAuth2User(userRequest, oAuth2User);
        }
        catch (AuthenticationException e)
        {
            throw e;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    private OAuth2User checkOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User)
    {
        OAuth2UserDetails oAuth2UserDetails = OAuth2UserDetailsFactory
                .createOAuth2UserDetails(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes());

        if(ObjectUtils.isEmpty(oAuth2UserRequest))
        {
            throw new RuntimeException("Cannot identifty OAuth2 user!");
        }

        User user = usersDao.findByUsernameAndProvider(
                oAuth2UserDetails.getEmail(),
                oAuth2UserRequest.getClientRegistration().getRegistrationId());
        User userDetails = null;
        if(user != null)
        {
            userDetails = user;
            userDetails = updateOAuth2UserDetail(userDetails, oAuth2UserDetails);
        }
        else
        {
            userDetails = registerOAuth2UserDetail(oAuth2UserRequest, oAuth2UserDetails);
        }
        return new OAuth2UserDetailsCustom(
                userDetails.getUserID(),
                userDetails.getUsername(),
                userDetails.getPassword(),
                Collections.singletonList(new SimpleGrantedAuthority(userDetails.getRole().getRoleName()))
        );
    }

    public User registerOAuth2UserDetail(OAuth2UserRequest oAuth2UserRequest, OAuth2UserDetails oAuth2UserDetails)
    {
        Role r = usersDao.findById(1L);
        User user = new User();
        user.setName(Objects.requireNonNullElse(oAuth2UserDetails.getName(), ""));
        user.setEmail(oAuth2UserDetails.getEmail());
        user.setProvider(oAuth2UserRequest.getClientRegistration().getRegistrationId());
        user.setRole(r);
        return usersDao.updateUser(user);
    }

    public User updateOAuth2UserDetail(User user, OAuth2UserDetails oAuth2UserDetails)
    {
        user.setEmail(oAuth2UserDetails.getEmail());
        return usersDao.mergeUser(user);
    }
}

По креирањето на објект од овој тип, се проверува во базата дали евентуално постои ваков корисник и доколку не, истиот се регистрира. Откако ќе се утврди неговиот статус, функцијата враќа објект од типот OAuth2UserDetailsCustom кој го имплементира UserDetails интерфејсот од Java со што се овозможува да поминува низ филтрите и да се третира исто како и регуларно најавен корисник директно на системот. Доколку најавата е успешна, како и претходно, корисникот се пренасочува кон React апликацијата.

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        getRedirectStrategy().sendRedirect(request, response, "http://localhost:3000/login-callback");
    }

Поврзува профил

Доколку корисникот сака да поврзе негов профил со друг профил регистриран на нашиот систем, тоа може да го направи на оваа страница со коректно внесување на корисничкото име и лозинката. Истите се испраќаат преку POST барање до серверската страна каде контролерот кој ги пречекува го повикува методот во сервисот во кој внесената лозинка, преку PasswordEncoder-от се проверува со хешираната која се чува во базата и доколку се точни ги поврзува профилите. Ова е имплементирано на тој начин што ентитетот User е во M:N релација сам со себе, односно за секој User се чува листа од User - connectedAccounts.

    @PostMapping("/users/{id}/connect")
    public ResponseEntity<?> connectAccount(@PathVariable Long id,
                                            @RequestParam String username,
                                            @RequestParam String password)
    {
        usersManager.connectAccount(id, username, password);
        return new ResponseEntity<>(HttpStatus.OK);
    }
    @Override
    public void connectAccount(Long id, String username, String password) {
        User u1 = findUserByID(id);
        User u2 = (User) loadUserByUsername(username);
        if(passwordEncoder.matches(password, u2.getPassword()))
        {
            u1.addConnectedUser(u2);
        }
        usersDao.updateUser(u1);
    }

Откако ќе заврши оваа постапка, најавениот корисник ќе има пристап и до ресурсите на профилот со кој е поврзан.

Систем

Испраќа известување до администратори

Оваа функционалност предвидува испраќања на нотификација/потсетник до администарторите во случај кога ќе се соберат повеќе профили/фирми за одобрување. За ова да функционира, потребно беше да имплементираме сервис за испраќање пораки, кој ја користи Spring Email библиотеката.

@Service
public class MailingServiceImpl implements MailingService {

    private final JavaMailSender emailSender;

    public MailingServiceImpl(JavaMailSender emailSender) {
        this.emailSender = emailSender;
    }

    @Override
    public void sendMail(String to, String subject, String messageText) {
        SimpleMailMessage mail = new SimpleMailMessage();
        mail.setFrom("TourMate");
        mail.setTo(to);
        mail.setSubject(subject);
        mail.setText(messageText);
        emailSender.send(mail);
    }
}

Оваа проверка се прави во одредено време, секој понеделник на полноќ и доколку постојат фирми кои не се одобрени, се испраќа порака до сите администратори како потсетување. За да може во позадина да се извршуваат вакви задчаи потребно беше апликацијата да ја анотираме со @EnableScheduling, а компонентата изгледа вака:

@Component
public class MailJobs {
    @Autowired
    private BusinessManager businessManager;
    @Autowired
    private UsersManager usersManager;
    @Autowired
    private MailingService mailingService;

    @Scheduled(cron = "0 0 * * * MON")
    public void execute() {
        List<Business> unapprovedBusinesses = businessManager.getUnapprovedBusinesses();
        if(!unapprovedBusinesses.isEmpty())
        {
            List<User> admins = usersManager.getAdmins().stream().filter(x -> x.getEmail().contains("@")).toList();
            String subject = "Бизниси кои чекаат на одобрување";
            String message = "Почитувани,\n\nВе известуваме дека постојат повеќе бизнис кои чекаат на Ваша одлука за одобрување.\n\nСо почит,\n\nTourMate";
            admins.forEach(x -> mailingService.sendMail(x.getEmail(), subject, message));
        }
    }
}

Attachments (2)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.