Közel egy hónapot dolgoztam a személyes weboldalamon, és sajnos elvéreztem a deployment résznél. Közben egyre frusztráltabb lettem, mivel az idő csak telt, és hiába működik minden tökéletesen localhoston, ha egyszer nem tudom megosztani a nagyvilággal. A domain, tárhely, vServer nincs ingyen, utóbbit időközben le is mondtam. Eleinte személyes kudarcként éltem meg a dolgot, de valójában rengeteget tanultam belőle.

Eredetileg egy olyan weboldalt szerettem volna készíteni, amely részben személyes blogként, részben pedig szakmai életrajzként funkcionál. Szerettem volna mindenképpen recepteket, és kertészeti témájú bejegyzéseket közzétenni, ahol a felhasználó bizonyos növényekre, hozzávalókra szűrve szezonálisan elkészíthető dolgokat kap találatként. Mivel a fókusz mégis a blog volt, ezért fontos volt még, hogy a felhasználó regisztrálni tudjon, és hozzászólásokat írni, a fiókját kezelni, kedvenc recepteket, bejegyzéseket hozzáadni. Jogosultságoktól függően egyéni felhasználói felület. Mindent, amit szerettem volna, sikerült beépítenem. Mindezt biztonságos módon, ennél a résznél nagyon jó szolgálatot tett a Spring Security.

A problémák akkor kezdődtek, amikor a kész projektet szerettem volna közzétenni. Eleinte az FTP tárhelyemre próbáltam feltölteni egy war file-t az alábbi tutorial alapján:

https://www.piotrnowicki.com/2012/10/creating-maven-repository-on-shared-hosting/

Ez sajnos nem működött, mert a hosting cégnél mint ez utólag kiderült, nem lehetséges. Végül béreltem egy vServert. Ez egyébként a tökéletes megoldás, ha az ember ért a szerverekhez. Sikerült némi szerencsétlenkedés árán telepítenem az Apache Tomcatet, de sajnos nem tudtam futtatni a war file-t úgy sem:

https://stackoverflow.com/questions/77931241/troubleshooting-deployment-issues-with-spring-boot-war-on-tomcat-11-and-jdk-21-o/78059647#78059647

Később a konténerizált Backendem már futott, de a nagy egészet nem sikerült megoldanom (kívülről nem volt elérhető a végpont), és közben állandóan amiatt aggódtam, hogy a szerverekhez való hozzá nem értésem miatt biztonsági kockázatoknak lenne úgyis a weboldal kitéve.

A vServer abszolút mélyvíz, külön tudomány. Tényleg annyira komplex téma, hogy utólag nem is értem, mit gondoltam nagyjából egy éves programozói „tudással“. Summa summarum: magasra tettem a lécet, és nem sikerült átugranom.

Tech Stack

Backend: Java, Maven, Spring Boot, REST API, Spring Security, Hibernate, Lombok, Java Mail Sender.

Adatbázis: kezdetben H2, később MariaDB – HeidiSQL

Frontend: Vue 3 Composition API, Vuetify, Pinia, Axios, Quill.

Egyéb: GitLab, Docker

A Backendről részletesebben

Természetesen nem fogom itt az összes kódot megosztani, mert nem akarok kisregényt írni, de szemezgetnék néhány érdekesebb részből.

A UserEntity osztályban definiáltam minden olyan változót, amely egy felhasználó szempontjából releváns lehet. Sokkal elegánsabb egyébként, ha az ember külön táblában tárolja a regisztrációhoz/bejelentkezéshez szükséges dolgokat, de én egyben oldottam meg a dolgokat.

@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "Users")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "userId")
public class UserEntity implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false, updatable = false, unique = true)
    private UUID userId;

    @Column(nullable = false, unique = true)
    private String username;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserRole userRole;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private LocalDate registrationDate;

    private LocalDate lastLoginDate;

    @Column
    boolean isEnabled;

    @Column
    private String confirmationToken;

    @Column
    private LocalDate tokenExpirationDate;

    @Column
    private String avatarUrl = "https://valami.com/assets/alapertelmezettprofilkep.jpg";


    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonManagedReference
    private Set<ArticleEntity> articles = new HashSet<>();

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonManagedReference
    private Set<RecipeEntity> recipes = new HashSet<>();

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonIgnore
    private Set<FavouriteRecipeEntity> favouriteRecipes = new HashSet<>();

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(new SimpleGrantedAuthority(userRole.toString()));
        return authorityList;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.isEnabled;
    }
}

A token frissitése az alábbi módon történik:

@Service
public class TokenRefreshService {

    private final TokenService tokenService;

    public TokenRefreshService(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    public String refreshTokenIfNeeded(String token) {
        if (tokenService.isTokenExpired(token)) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            UserEntity userEntity = (UserEntity) authentication.getPrincipal();
            return tokenService.generateTokenWithClaims(userEntity);
        }
        return token;
    }
}

Fontos megjegyezni, hogy kétféle tokent használok, a JWT Webtokent a böngésző Local Storage-ben tárolom, és az azonosítás után olyan információkat tartalmaz, mint például a jogosultsági szint, stb. 24 órás lejárati idővel. A másik token arra szolgál, hogy regisztrációkor e-mailben került elküldésre a felhasználónak, akinek a linkre kattintva meg kellett erősíteni a regisztrációt. Sikeres regisztrációt követően az isEnabled boolean érték true-ra változott, és a felhasználó be tudott lépni a fiókjába.

A UserService osztály a következő módon néz ki:

@Service
public class UserService {

    @Autowired
    UserCrudRepository userCrudRepository;

    @Autowired
    ConversionService conversionService;

    @Autowired
    TokenService tokenService;

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    EmailService emailService;

    @Autowired
    PasswordEncoder passwordEncoder;

    public AuthDTO login(LoginDTO loginDTO) {
        UserEntity userEntity = getUserByUsernameOrEmail(loginDTO);
        String username = userEntity.getUsername();

        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, loginDTO.getPassword()));

        String jwt = tokenService.generateTokenWithClaims(userEntity);

        return new AuthDTO(
                userEntity.getUserId(),
                userEntity.getUsername(),
                userEntity.getEmail(),
                userEntity.getUserRole().getLabel(),
                jwt);
    }

    public AuthDTO register(RegisterDTO registerDTO) throws UserAlreadyExistsException {

        String confirmationToken = UUID.randomUUID().toString();
        LocalDate tokenExpirationDate = LocalDate.now().plusDays(1); // Például 1 napos lejárati idő

        UserEntity userEntity = UserEntity.builder()
                .userId(UUID.randomUUID())
                .username(registerDTO.getUsername())
                .userRole(registerDTO.getUserRole())
                .password(passwordEncoder.encode(registerDTO.getPassword()))
                .email(registerDTO.getEmail())
                .registrationDate(LocalDate.now())
                .lastLoginDate(LocalDate.now())
                .confirmationToken(confirmationToken)
                .tokenExpirationDate(tokenExpirationDate)
                .build();

        userCrudRepository.save(userEntity);

        emailService.sendSimpleEmail(
                userEntity.getEmail(),
                "Bestätigung der Registrierung",
                "Bitte klicken Sie auf den folgenden Link, um Ihre Registrierung zu bestätigen: " +
                        "http://localhost:8080/api/auth/confirm?token=" + confirmationToken
        );

        String jwt = tokenService.generateTokenWithClaims(userEntity);

        return new AuthDTO(
                userEntity.getUserId(),
                userEntity.getUsername(),
                userEntity.getEmail(),
                userEntity.getUserRole().getLabel(),
                jwt
        );
    }

    public UserEntity getUserByUsernameOrEmail(LoginDTO loginDTO) {
        return getUserFromOptional(
                userCrudRepository.findByUsernameOrEmail(loginDTO.getUsername(), loginDTO.getUsername())
        );
    }

    public void confirmRegistration(String token) throws Exception {
        Optional<UserEntity> userOptional = userCrudRepository.findByConfirmationToken(token);

        if (userOptional.isPresent()) {
            UserEntity user = userOptional.get();
            // Ellenőrzi, hogy a felhasználó már aktivált-e
            if (user.isEnabled()) {
                throw new Exception("Benutzer ist schon aktiviert.");
            }
            // Ellenőrzi, hogy a token még nem járt-e le
            if (user.getTokenExpirationDate().isBefore(LocalDate.now())) {
                throw new Exception("Token ist abgelaufen.");
            }
            user.setEnabled(true);
            user.setConfirmationToken(null); // Törli a megerősítő tokent
            user.setTokenExpirationDate(null); // Törli a token lejárati dátumát
            userCrudRepository.save(user);
        } else {
            throw new Exception("Token ist ungültig.");
        }
    }

    public UserEntity getUserFromOptional(Optional<UserEntity> userEntityOptional) {
        UserEntity userEntity;
        try {
            userEntity = conversionService.getEntityFromOptional(userEntityOptional);
        } catch (EmptyOptionalException e) {
            throw new UsernameNotFoundException("Kein entsprechender User in der Datenbank gefunden!");
        }
        return userEntity;
    }

    public ResponseUserDTO createUser(UserDTO userDTO) {
        // Átalakítja a UserDTO-t UserEntity-re
        UserEntity userEntity = convertUserDTOToEntity(userDTO);

        // Menti a felhasználót a UserEntitiy-be
        UserEntity savedUser = userCrudRepository.save(userEntity);

        // Átalakítja a mentett UserEntity-t ResponseUserDTO-ra
        return convertUserEntityToResponseUserDTO(savedUser);
    }

    public ResponseUserDTO getUserById(UUID userId) {
        UserEntity userEntity = userCrudRepository.findByUserId(userId)
                .orElseThrow(() -> new NotFoundException("Benutzer nicht gefunden"));

        // Átalakítja a UserEntity-t to ResponseUserDTO-ra
        return convertUserEntityToResponseUserDTO(userEntity);
    }

    public UserEntity getUserByUsername(String username) {
        return userCrudRepository.findUserByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Benutzer nicht gefunden mit Benutzernamen: " + username));
    }


    public List<ResponseUserDTO> getAllUsers() {
        // Retrieve all UserEntities
        List<UserEntity> userEntities = (List<UserEntity>) userCrudRepository.findAll();

        // Convert List<UserEntity> to List<ResponseUserDTO>
        return userEntities.stream()
                .map(this::convertUserEntityToResponseUserDTO)
                .collect(Collectors.toList());
    }

    public ResponseUserDTO editUserById(UUID userId, UserDTO userDTO) {
        UserEntity existingUser = userCrudRepository.findByUserId(userId)
                .orElseThrow(() -> new NotFoundException("Benutzer nicht gefunden"));

        // Frissíti a már létező felhasználót - existingUser-t a DTO adataival
        updateUserEntity(existingUser, userDTO);

        // Elmenti a frissített felhasználói adatokat
        UserEntity updatedUser = userCrudRepository.save(existingUser);

        // Átalakítja a frissített UserEntity-t ResponseUserDTO-vá
        return convertUserEntityToResponseUserDTO(updatedUser);
    }

    @Transactional
    public void deleteUserById(UUID userId) {
        // Ellenőrzi, hogy a felhasználó létezik-e.
        if (!userCrudRepository.existsByUserId(userId)) {
            throw new NotFoundException("Benutzer nicht gefunden.");
        }

        // Törli a felhasználót a kapott userId alapján
        userCrudRepository.deleteByUserId(userId);
    }

    private UserEntity convertUserDTOToEntity(UserDTO userDTO) {
        return UserEntity.builder()
                .username(userDTO.getUsername())
                .userRole(userDTO.getUserRole())
                .password(passwordEncoder.encode(userDTO.getPassword()))
                .email(userDTO.getEmail())
                .registrationDate(userDTO.getRegistrationDate())
                .lastLoginDate(userDTO.getLastLoginDate())
                .build();
    }

    private ResponseUserDTO convertUserEntityToResponseUserDTO(UserEntity userEntity) {
        return ResponseUserDTO.builder()
                .userId(userEntity.getUserId())
                .username(userEntity.getUsername())
                .userRole(userEntity.getUserRole())
                .email(userEntity.getEmail())
                .registrationDate(userEntity.getRegistrationDate())
                .lastLoginDate(userEntity.getLastLoginDate())
                .build();
    }

    private void updateUserEntity(UserEntity existingUser, UserDTO userDTO) {
        existingUser.setUsername(userDTO.getUsername());
        existingUser.setUserRole(userDTO.getUserRole());
        existingUser.setEmail(userDTO.getEmail());
        existingUser.setRegistrationDate(userDTO.getRegistrationDate());
        existingUser.setLastLoginDate(userDTO.getLastLoginDate());

        // Frissítse a jelszót, de csak akkor, ha meg van adva a userDTO-ban
        if (userDTO.getPassword() != null) {
            existingUser.setPassword(passwordEncoder.encode(userDTO.getPassword()));
        }
    }
}

A Frontendről részletesebben

Nagyon szeretem a Vue-t, mert lehetővé teszi, hogy a komponenseket újra és újra felhasználjam, ezzel rengeteg időt és energiát megtakarítottam a fejlesztés során. Továbbá az egyik legkönnyebben tanulható Framework.

Alapvetően imádok CSS-ben molyolni, én csak „pixelpöcögtetésnek“ hívom az ilyesmit, de a Vuetify látványos, ízléses, letisztult és korszerű „konyhakész“ megoldásokat kínál, és a Bootstraphez hasonlóan egyszerű használni, és szintén jó a dokumentáció hozzá.

A végpontok lekérdezése az Axios HTTP klienskönyvtár segítségével történik, az Axios telepítése egyébként gyerekjáték:

npm install axios

A JSON formátumban kapott válaszokat Pinia állapotkezelő könyvtár segítségével úgy nevezett store-okban tárolom tematikusan (AuthStore, UserStore, ArticleStore, RecipeStore, stb), és a közvetlenül hozzájuk tartozó frontend logikát szintén. Nagyban megkönnyíti az alkalmazás egyszerű, de mégis hatékony állapotkezelését. A Piniát érdemes már a projekt legelején hozzáadni, a Vue telepítéskor egyébként azonnal felajánlja, mint lehetőséget.

Időnként kihívást jelentett, hogy bizonyos komponensek nem mindig frissültek megfelelően, de a watcherek, computed tulajdonságok, és bizonyos hookok megfelelő használata megoldotta végül minden reaktivitási problémámat.

Minden, amit eredetileg megálmodtam, és elképzeltem, tökéletesen működött Frontend oldalon is. Ahogyan azt már az elején említettem, a problémát a deployment okozta. Egy ideig próbálkoztam szorgalmasan, végül lemondtam inkább a vServert, és feldobtam gyorsan az aktuális WordPresst a tárhelyemre.

Így kötöttem ki végül a WordPressnél

Szeretném leszögezni, hogy nem vagyok túl nagy WordPress fan, de azt el kell ismerni, hogy baromi kényelmes az egész, milliónyi hasznos pluginnal, modern és ízléses sablonokkal, a nagy részük ráadásul ingyenesen elérhető. Egy bizonyos pontig olyan egyszerűen konfigurálható, még csak programozni sem kell különösebben tudni hozzá, általános szövegértési képességekkel fantasztikusan dolgokat lehet belőle kihozni.

De ami ez előnye, az a hátránya is. Én szeretem a dolgokat nagyon aprólékosan a magam ízlése szerint formálni, és erre a WordPress gyakran nem alkalmas. HTML, CSS és PHP tudással egész jól alakítható, de ennek ellenére megvannak a maga korlátai. Sajnos az a szabadság egyáltalán nincs meg, amit egy saját magam által írt Backend / Frontend ad, de cserébe egy bejáratott, régóta jól bevált, viszonylag stresszmentes tartalomkezelő rendszer.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Trending