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:
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