diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b44df56234ecbb7871237cdad4f35e5fb3d22ebf..20e82f781093ce7c81eb104f12e2244cee903a95 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,11 +1,29 @@
-# Based on https://gitlab.fi.muni.cz/unix/examples/ci-examples/-/blob/java-maven/.gitlab-ci.yml
-image: maven:3-openjdk-17
-
+image: 'maven:3.8.5-openjdk-17-slim'
+cache:
+  paths:
+    - .m2/repository
+variables:
+  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
+  MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
+stages:
+  - build
+  - test
 build:
   tags:
     - shared-fi
+  stage: build
+  script:
+    - echo "We are building your project, $GITLAB_USER_LOGIN"
+    - mvn clean install -Dmaven.test.skip=true $MAVEN_CLI_OPTS
+test:
+  tags:
+    - shared-fi
+  stage: test
   script:
-    - mvn -ntp clean package
+    - echo "Starting tests ..."
+    - mvn test
+
+
 
 
 
diff --git a/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java b/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java
index 17a93eb3d9d3388b651b81d4fcf96a3141d418db..55fec5d0884d4191b45a32adfea9a4aa746d6bb1 100644
--- a/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java
+++ b/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java
@@ -9,75 +9,133 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.util.List;
-
+/**
+ * A generic service for domain objects.
+ *
+ * @author Marek Skácelík
+ * @param <T> the type of the domain object.
+ */
 public abstract class DomainService<T extends DomainObject> {
 
-  public static final int DEFAULT_PAGE_SIZE = 10;
+	public static final int DEFAULT_PAGE_SIZE = 10;
 
-  public abstract JpaRepository<T, String> getRepository();
+	/**
+	 * Returns the repository for the domain object.
+	 *
+	 * @return the repository.
+	 */
+	public abstract JpaRepository<T, String> getRepository();
 
-  @Transactional
-  public T create(T entity)
-  {
-    entity.createdDateTime = LocalDateTime.now();
-    return getRepository().save(entity);
-  }
+	/**
+	 * Creates the given entity.
+	 *
+	 * @param entity the entity to create.
+	 * @return the created entity.
+	 */
+	@Transactional
+	public T create(T entity) {
+		entity.createdDateTime = LocalDateTime.now();
+		return getRepository().save(entity);
+	}
 
-  @Transactional(readOnly = true)
-  public Page<T> findAllPageable(Pageable pageable) {
-    return getRepository().findAll(pageable);
-  }
+	/**
+	 * Returns all entities as a pageable list.
+	 *
+	 * @param pageable the pageable parameters.
+	 * @return a pageable list of all entities.
+	 */
+	@Transactional(readOnly = true)
+	public Page<T> findAllPageable(Pageable pageable) {
+		return getRepository().findAll(pageable);
+	}
 
-  @Transactional(readOnly = true)
-  public List<T> findAll() {
-    return getRepository().findAll().stream().filter(entity -> entity.deletedDateTime == null).toList();
-  }
+	/**
+	 * Returns all entities.
+	 *
+	 * @return a list of all entities.
+	 */
+	@Transactional(readOnly = true)
+	public List<T> findAll() {
+		return getRepository().findAll().stream().filter(entity -> entity.deletedDateTime == null).toList();
+	}
 
-  @Transactional(readOnly = true)
-  public Page<T> findAllPageableInt(int page) {
-    return getRepository().findAll(PageRequest.of(page, DEFAULT_PAGE_SIZE))/*.filter(entity -> entity.deletedDateTime == null)*/;
-  }
+	/**
+	 * Returns all entities as a pageable list.
+	 *
+	 * @param page the page number.
+	 * @return a pageable list of all entities.
+	 */
+	@Transactional(readOnly = true)
+	public Page<T> findAllPageableInt(int page) {
+		return getRepository().findAll(PageRequest.of(page, DEFAULT_PAGE_SIZE))/*.filter(entity -> entity.deletedDateTime == null)*/;
+	}
 
-  @Transactional(readOnly = true)
-  public T findById(String id) {
-    return getRepository()
-            .findById(id)
-            .filter(entity -> entity.deletedDateTime == null)
-        .orElseThrow(() -> new EntityNotFoundException("Entity with '" + id + "' not found."));
-  }
+	/**
+	 * Returns the entity with the given ID.
+	 *
+	 * @param id the ID of the entity to retrieve.
+	 * @return the entity with the given ID.
+	 * @throws EntityNotFoundException if the entity does not exist.
+	 */
+	@Transactional(readOnly = true)
+	public T findById(String id) {
+		return getRepository()
+				.findById(id)
+				.filter(entity -> entity.deletedDateTime == null)
+				.orElseThrow(() -> new EntityNotFoundException("Entity with '" + id + "' not found."));
+	}
 
-  @Transactional
-  public void deleteAll()
-  {
-    List<T> entities = findAll();
-    entities.stream().map(entity -> entity.deletedDateTime = LocalDateTime.now());
-    getRepository().saveAll(entities);
-  }
+	/**
+	 * Deletes all entities by setting their deletion datetime.
+	 */
+	@Transactional
+	public void deleteAll() {
+		List<T> entities = findAll();
+		entities.stream().map(entity -> entity.deletedDateTime = LocalDateTime.now());
+		getRepository().saveAll(entities);
+	}
 
-  @Transactional
-  public T deleteById(String id) {
-    T entity = findById(id);
-    if (entity == null)
-      throw new EntityNotFoundException("Entity '" + id + "' not found.");
-    entity.deletedDateTime = LocalDateTime.now();
-    getRepository().save(entity);
-    return entity;
-  }
+	/**
+	 * Deletes the entity with the given ID by setting its deletion datetime.
+	 *
+	 * @param id the ID of the entity to delete.
+	 * @return the deleted entity.
+	 * @throws EntityNotFoundException if the entity does not exist.
+	 */
+	@Transactional
+	public T deleteById(String id) {
+		T entity = findById(id);
+		if (entity == null)
+			throw new EntityNotFoundException("Entity '" + id + "' not found.");
+		entity.deletedDateTime = LocalDateTime.now();
+		getRepository().save(entity);
+		return entity;
+	}
 
-  @Transactional
-  public T update(T entityToUpdate, String id) {
-    T entity = findById(id);
-    if (entity == null)
-      throw new EntityNotFoundException("Entity '" + id + "' not found.");
-    // TODO: change when ORM tool available
-    entityToUpdate.setId(id);
-    getRepository().save(entityToUpdate);
-    return entity;
-  }
+	/**
+	 * Updates the entity with the given ID.
+	 *
+	 * @param entityToUpdate the updated entity.
+	 * @param id             the ID of the entity to update.
+	 * @return the updated entity.
+	 * @throws EntityNotFoundException if the entity does not exist.
+	 */
+	@Transactional
+	public T update(T entityToUpdate, String id) {
+		T entity = findById(id);
+		if (entity == null)
+			throw new EntityNotFoundException("Entity '" + id + "' not found.");
+		// TODO: change when ORM tool available
+		entityToUpdate.setId(id);
+		getRepository().save(entityToUpdate);
+		return entity;
+	}
 
-  @Transactional
-  public void deleteAllHardDelete()
-  {
-    getRepository().deleteAll();
-  }
+	/**
+	 * Deletes all entities from the database without soft-delete functionality.
+	 */
+	@Transactional
+	public void deleteAllHardDelete() {
+		getRepository().deleteAll();
+	}
 }
diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java b/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java
index 56bd7cb344f1f31fcd26ac45a8231eb553a98bd2..317a298869e35105b831a027539e4c17ca7f7d35 100644
--- a/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java
+++ b/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java
@@ -1,10 +1,7 @@
 package cz.muni.fi.pa165.core.user;
 
+import cz.muni.fi.pa165.model.dto.user.*;
 import cz.muni.fi.pa165.model.dto.common.Result;
-import cz.muni.fi.pa165.model.dto.user.UserCreateDto;
-import cz.muni.fi.pa165.model.dto.user.UserDto;
-import cz.muni.fi.pa165.model.dto.user.UserStatisticsCreateDto;
-import cz.muni.fi.pa165.model.dto.user.UserUpdateDto;
 import jakarta.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import io.swagger.v3.oas.annotations.Operation;
@@ -14,6 +11,8 @@ import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -39,74 +38,75 @@ public class UserController {
 		this.userService = userService;
 	}
 
-  @Operation(
-      summary = "Find user by ID",
-      description = "Returns a single user",
-      tags = {"user"})
-  @ApiResponses(
-      value = {
-        @ApiResponse(
-            responseCode = "200",
-            description = "User found",
-            content = {
-              @Content(
-                  mediaType = "application/json",
-                  schema = @Schema(implementation = UserDto.class))
-            }),
-        @ApiResponse(responseCode = "404", description = "User not found", content = @Content)
-      })
-  @GetMapping("/{id}")
-  public UserDto findById(
-      @Parameter(description = "ID of user to be searched") @PathVariable String id) {
-    return userFacade.findById(id);
-  }
-
-  @Operation(
-      summary = "Create user",
-      description = "Creates a new user",
-      tags = {"user"})
-  @ApiResponses(
-      value = {
-        @ApiResponse(
-            responseCode = "201",
-            description = "User created",
-            content = {
-              @Content(
-                  mediaType = "application/json",
-                  schema = @Schema(implementation = UserDto.class))
-            }),
-        @ApiResponse(responseCode = "400", description = "Invalid input", content = @Content),
-        @ApiResponse(
-            responseCode = "409",
-            description = "User with the same name already exists",
-            content = @Content)
-      })
-  @PostMapping
-  public UserDto create(
-      @Parameter(description = "User to be created") @RequestBody UserCreateDto userCreateDto) {
-    return userFacade.create(userCreateDto);
-  }
-
-  @Operation(
-      summary = "Get all users",
-      description = "Returns all users",
-      tags = {"user"})
-  @ApiResponses(
-      value = {
-        @ApiResponse(
-            responseCode = "200",
-            description = "Users found",
-            content = {
-              @Content(
-                  mediaType = "application/json",
-                  schema = @Schema(implementation = Result.class))
-            })
-      })
-  @GetMapping
-  public Result<UserDto> findAll(
-      @Parameter(description = "Page number of results to retrieve") @RequestParam int page) {
-    return userFacade.findAll(page);
-  }
+	@Operation(
+			summary = "Find user by ID",
+			description = "Returns a single user",
+			tags = {"user"})
+	@ApiResponses(
+			value = {
+					@ApiResponse(
+							responseCode = "200",
+							description = "User found",
+							content = {
+									@Content(
+											mediaType = "application/json",
+											schema = @Schema(implementation = UserDto.class))
+							}),
+					@ApiResponse(responseCode = "404", description = "User not found", content = @Content)
+			})
+	@GetMapping("/{id}")
+	public UserDto findById(
+			@Parameter(description = "ID of user to be searched") @PathVariable String id) {
+		return userFacade.findById(id);
+	}
+
+	@Operation(
+			summary = "Create user",
+			description = "Creates a new user",
+			tags = {"user"})
+	@ApiResponses(
+			value = {
+					@ApiResponse(
+							responseCode = "201",
+							description = "User created",
+							content = {
+									@Content(
+											mediaType = "application/json",
+											schema = @Schema(implementation = UserDto.class))
+							}),
+					@ApiResponse(responseCode = "400", description = "Invalid input", content = @Content),
+					@ApiResponse(
+							responseCode = "409",
+							description = "User with the same name already exists",
+							content = @Content)
+			})
+	@PostMapping
+	public ResponseEntity<UserDto> create(
+			@Parameter(description = "User to be created") @RequestBody UserCreateDto userCreateDto) {
+		return ResponseEntity.status(HttpStatus.CREATED).body(userFacade.create(userCreateDto));
+
+	}
+
+	@Operation(
+			summary = "Get all users",
+			description = "Returns all users",
+			tags = {"user"})
+	@ApiResponses(
+			value = {
+					@ApiResponse(
+							responseCode = "200",
+							description = "Users found",
+							content = {
+									@Content(
+											mediaType = "application/json",
+											schema = @Schema(implementation = Result.class))
+							})
+			})
+	@GetMapping
+	public Result<UserDto> findAll(
+			@Parameter(description = "Page number of results to retrieve") @RequestParam int page) {
+		return userFacade.findAll(page);
+	}
 
 	@PutMapping("/{id}")
 	@Operation(summary = "Update a user by ID")
@@ -166,7 +166,49 @@ public class UserController {
 		return userFacade.findAll();
 	}
 
+
+	@Operation(summary = "Log in a user", tags = {"user"})
+	@ApiResponses(value = {
+			@ApiResponse(responseCode = "200", description = "Successful login"),
+			@ApiResponse(responseCode = "401", description = "Invalid credentials")
+	})
+	@PostMapping("/login")
+	public ResponseEntity<?> login(@RequestBody LoginInfoDto request) {
+		return userFacade.login(request);
+	}
+
+	@Operation(summary = "Log out a user", tags = {"user"})
+	@ApiResponses(value = {
+			@ApiResponse(responseCode = "200", description = "Successful logout"),
+			@ApiResponse(responseCode = "401", description = "Unauthorized")
+	})
+	@PostMapping("/logout")
+	public ResponseEntity<?> logout() {
+		return userFacade.logout();
+	}
+
+	@PutMapping("/password/{userId}")
+	@Operation(summary = "Change user password",
+			description = "Change the password for the specified user.",
+			tags = {"user"}
+	)
+	@ApiResponses(value = {
+			@ApiResponse(responseCode = "200", description = "Password changed successfully."),
+			@ApiResponse(responseCode = "400", description = "Bad request.", content = @Content),
+			@ApiResponse(responseCode = "401", description = "Unauthorized.", content = @Content),
+			@ApiResponse(responseCode = "404", description = "User not found.", content = @Content),
+			@ApiResponse(responseCode = "500", description = "Internal server error.", content = @Content)
+	})
+	public ResponseEntity<?> changePassword(
+			@Parameter(description = "The ID of the user whose password will be changed.", required = true)
+			@PathVariable String userId,
+			@Parameter(description = "The old and new password information.", required = true)
+			@Valid @RequestBody ChangePasswordDto request) {
+		return userFacade.changePassword(request, userId); // userId from JWT token.
+	}
 	/*
 		TODO: get user with roles
 	 */
 }
+
+
diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java
index 7abd7d802a79d6ec5803110a059667f26db9bed8..a31277ef80b5d682a8b8b840312cac87e434cf75 100644
--- a/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java
+++ b/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java
@@ -1,16 +1,15 @@
 package cz.muni.fi.pa165.core.user;
 
 import cz.muni.fi.pa165.core.common.DomainFacade;
-import cz.muni.fi.pa165.model.dto.user.UserCreateDto;
-import cz.muni.fi.pa165.model.dto.user.UserDto;
-import cz.muni.fi.pa165.model.dto.user.UserUpdateDto;
+import cz.muni.fi.pa165.model.dto.user.*;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
+
+import static cz.muni.fi.pa165.core.user.utils.PasswordUtil.isPasswordValid;
 
 
 @Component
-@Transactional
 public class UserFacade extends DomainFacade<User, UserDto, UserCreateDto, UserUpdateDto> {
 
 	private final UserService userService; // For the "UserService" specific methods
@@ -23,4 +22,43 @@ public class UserFacade extends DomainFacade<User, UserDto, UserCreateDto, UserU
 		this.userMapper = userMapper;
 	}
 
+	public ResponseEntity<?> logout() {
+		// validate JWT token...
+		return ResponseEntity.ok("Logged out successfully.");
+	}
+
+	public ResponseEntity<?> login(LoginInfoDto request) {
+		String username = request.getUsername();
+		String password = request.getPassword();
+		if (username.isBlank()) {
+			ResponseEntity.badRequest().body("Username is empty");
+		}
+		if (!isPasswordValid(password)) {
+			ResponseEntity.badRequest().body("password is not valid");
+		}
+		return ResponseEntity.ok("ok!");
+	}
+
+	public ResponseEntity<?> changePassword(ChangePasswordDto request, String userId){
+		String oldPassword = request.getOldPassword();
+		String newPassword = request.getNewPassword();
+		String confirmPassword = request.getNewPasswordConfirmation();
+
+		// Check if new password is valid
+
+		if (!isPasswordValid(newPassword)) {
+			return ResponseEntity.badRequest().body("New password is invalid.");
+		}
+
+		// Check if new password matches confirmation password
+		if (!newPassword.equals(confirmPassword)) {
+			return ResponseEntity.badRequest().body("New password and confirmation password do not match.");
+		}
+
+		// TODO: Check if old password matches user's current password
+
+		// TODO: Update user's password in the database
+
+		return ResponseEntity.ok("Password changed successfully.");
+	}
 }
diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/utils/PasswordUtil.java b/core/src/main/java/cz/muni/fi/pa165/core/user/utils/PasswordUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..70c55ba9cd940d1b68c4a419eead17206639bd65
--- /dev/null
+++ b/core/src/main/java/cz/muni/fi/pa165/core/user/utils/PasswordUtil.java
@@ -0,0 +1,26 @@
+package cz.muni.fi.pa165.core.user.utils;
+
+public class PasswordUtil {
+	public static boolean  isPasswordValid(String password) {
+		// check for minimum length of 8 characters
+		if (password.length() < 8) {
+			return false;
+		}
+
+		// check for white spaces
+		if (password.chars().anyMatch(Character::isWhitespace)) {
+			return false;
+		}
+
+		// check for at least one uppercase letter
+		if (!password.matches(".*[A-Z].*")) {
+			return false;
+		}
+
+		// check for at least one lowercase letter
+		if (!password.matches(".*[a-z].*")) {
+			return false;
+		}
+		return true;
+	}
+}
diff --git a/core/src/test/java/cz/muni/fi/pa165/core/user/UserControllerTest.java b/core/src/test/java/cz/muni/fi/pa165/core/user/UserControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..751cab2a39bff9274c4239364df1ddb9da039aab
--- /dev/null
+++ b/core/src/test/java/cz/muni/fi/pa165/core/user/UserControllerTest.java
@@ -0,0 +1,65 @@
+package cz.muni.fi.pa165.core.user;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.muni.fi.pa165.model.dto.user.UserCreateDto;
+import cz.muni.fi.pa165.model.dto.user.UserDto;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Integration tests for the {@link UserController} class.
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+public class UserControllerTest {
+
+	@Autowired
+	private MockMvc mockMvc;
+
+	@Autowired
+	private ObjectMapper objectMapper;
+
+	private final static String URL = "/api/user";
+	private final static String CONTENT_TYPE =  "application/json";
+
+	/**
+	 * Tests the {@link UserController#create(UserCreateDto)} method with valid input.
+	 * Expects the response status code to be 201 (Created).
+	 */
+	@Test
+	@DisplayName("Create user with valid input")
+	void createNonExistingUserTest() throws Exception {
+		// Prepare
+		UserCreateDto createDto = new UserCreateDto();
+		createDto.setUsername("testUser");
+		createDto.setPassword("testPassword");
+		createDto.setFirstName("John");
+		createDto.setLastName("Doe");
+		createDto.setEmail("johndoe@test.com");
+
+		// Execute
+		String response = mockMvc.perform(post(URL)
+						.contentType(CONTENT_TYPE)
+						.content(objectMapper.writeValueAsString(createDto)))
+				.andExpect(status().isCreated())
+				.andReturn()
+				.getResponse()
+				.getContentAsString();
+
+		// Verify
+		UserDto userDto = objectMapper.readValue(response, UserDto.class);
+		assertThat(userDto.getId()).isNotNull();
+		assertThat(userDto.getUsername()).isEqualTo(createDto.getUsername());
+		assertThat(userDto.getEmail()).isEqualTo(createDto.getEmail());
+		assertThat(userDto.getFirstName()).isEqualTo(createDto.getFirstName());
+		assertThat(userDto.getLastName()).isEqualTo(createDto.getLastName());
+	}
+}
diff --git a/model/src/main/java/cz/muni/fi/pa165/model/dto/user/ChangePasswordDto.java b/model/src/main/java/cz/muni/fi/pa165/model/dto/user/ChangePasswordDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..9eb9fdb213e6689f0162312a1bb040beff9cf598
--- /dev/null
+++ b/model/src/main/java/cz/muni/fi/pa165/model/dto/user/ChangePasswordDto.java
@@ -0,0 +1,12 @@
+package cz.muni.fi.pa165.model.dto.user;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class ChangePasswordDto {
+	private String oldPassword;
+	private String newPassword;
+	private String newPasswordConfirmation;
+}
diff --git a/model/src/main/java/cz/muni/fi/pa165/model/dto/user/LoginInfoDto.java b/model/src/main/java/cz/muni/fi/pa165/model/dto/user/LoginInfoDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0b86d2e8b275f740eded31e9a451964afb7cb40
--- /dev/null
+++ b/model/src/main/java/cz/muni/fi/pa165/model/dto/user/LoginInfoDto.java
@@ -0,0 +1,11 @@
+package cz.muni.fi.pa165.model.dto.user;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class LoginInfoDto {
+	private String username;
+	private String password;
+}