diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..cbfbd17 --- /dev/null +++ b/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + com.alterdekim.game + NoDice + 1.0-SNAPSHOT + jar + + + org.springframework.boot + spring-boot-starter-parent + 3.0.0 + + + + 1.8 + 1.8 + UTF-8 + + + + + log4j + log4j + 1.2.17 + + + org.springframework.boot + spring-boot-devtools + runtime + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + 3.1.1.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + + + mysql + mysql-connector-java + 8.0.33 + + + org.projectlombok + lombok + true + 1.18.28 + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-security + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/alterdekim/game/Application.java b/src/main/java/com/alterdekim/game/Application.java index 150c7c9..ccd1947 100644 --- a/src/main/java/com/alterdekim/game/Application.java +++ b/src/main/java/com/alterdekim/game/Application.java @@ -1,2 +1,24 @@ -package com.alterdekim.game;public class Application { -} +package com.alterdekim.game; + +import com.alterdekim.game.storage.StorageService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + CommandLineRunner init(StorageService storageService) { + return (args) -> { + storageService.init(); + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/alterdekim/game/controller/AuthController.java b/src/main/java/com/alterdekim/game/controller/AuthController.java new file mode 100644 index 0000000..fe002f0 --- /dev/null +++ b/src/main/java/com/alterdekim/game/controller/AuthController.java @@ -0,0 +1,83 @@ +package com.alterdekim.game.controller; + +import com.alterdekim.game.dto.UserDTO; +import com.alterdekim.game.entities.Invite; +import com.alterdekim.game.entities.User; +import com.alterdekim.game.service.InviteService; +import com.alterdekim.game.service.UserService; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +@Slf4j +@Controller +public class AuthController { + + private final String base_title = " | Nosedive"; + + private final UserService userService; + private final InviteService inviteService; + + public AuthController(UserService userService, InviteService inviteService) { + this.inviteService = inviteService; + this.userService = userService; + } + + @GetMapping("/game") + public String gamePage(Model model) { + return "game"; + } + + @GetMapping("/login") + public String loginPage(Model model) { + model.addAttribute("title", "Login" + base_title); + return "login"; + } + + @GetMapping("/access-denied") + public String accessDenied(Model model) { + model.addAttribute("title", "Access denied"); + return "access-denied"; + } + + @GetMapping("/signup") + public String showRegistrationForm(Model model) { + UserDTO userDto = new UserDTO(); + model.addAttribute("user", userDto); + return "signup"; + } + + @PostMapping("/signup") + public String registration( + @ModelAttribute("user") @Valid UserDTO userDto, + BindingResult result, + Model model) { + User existingUser = userService.findByUsername(userDto.getUsername()); + Invite existingInvite = inviteService.findById(1); + + if(existingUser != null && existingUser.getUsername() != null && !existingUser.getUsername().isEmpty() ){ + result.rejectValue("username", null, + "There is already an account registered with the same username"); + return "redirect:/signup?error=1"; + } + + if(!existingInvite.getInvite_code().equals(userDto.getInvite_code())) { + result.rejectValue("invite_code", null, "Incorrect invite code."); + return "redirect:/signup?error=1"; + } + + if(result.hasErrors()) { + model.addAttribute("user", new UserDTO()); + return "redirect:/signup?error=1"; + } + + userService.saveUser(userDto); + return "redirect:/"; + } +} + diff --git a/src/main/java/com/alterdekim/game/controller/FileServerController.java b/src/main/java/com/alterdekim/game/controller/FileServerController.java new file mode 100644 index 0000000..36f2cee --- /dev/null +++ b/src/main/java/com/alterdekim/game/controller/FileServerController.java @@ -0,0 +1,46 @@ +package com.alterdekim.game.controller; + +import com.alterdekim.game.storage.StorageFileNotFoundException; +import com.alterdekim.game.storage.StorageProperties; +import com.alterdekim.game.storage.StorageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@EnableConfigurationProperties(StorageProperties.class) +public class FileServerController { + + private final StorageService storageService; + + @Autowired + public FileServerController(StorageService storageService) { + this.storageService = storageService; + } + + @GetMapping("/static/{assetspath}/{filename:.+}") + @ResponseBody + public ResponseEntity serveFile(@PathVariable String filename, @PathVariable String assetspath) { + Resource file = storageService.loadAsResource("static/" + assetspath + "/" + filename); + if( assetspath.equals("images") ) { + return ResponseEntity.ok().contentType(MediaType.IMAGE_PNG).header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + file.getFilename() + "\"").body(file); + } + return ResponseEntity.ok().contentType(new MediaType("text", assetspath)).header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + file.getFilename() + "\"").body(file); + } + + @ExceptionHandler(StorageFileNotFoundException.class) + public ResponseEntity> handleStorageFileNotFound(StorageFileNotFoundException exc) { + return ResponseEntity.notFound().build(); + } +} + diff --git a/src/main/java/com/alterdekim/game/dto/UserDTO.java b/src/main/java/com/alterdekim/game/dto/UserDTO.java new file mode 100644 index 0000000..849723e --- /dev/null +++ b/src/main/java/com/alterdekim/game/dto/UserDTO.java @@ -0,0 +1,24 @@ +package com.alterdekim.game.dto; + +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class UserDTO { + private Long id; + @NotEmpty(message = "Username should not be empty") + private String username; + @NotEmpty(message = "Password should not be empty") + private String password; + @NotEmpty(message = "Invite code should not be empty") + private String invite_code; + + private String lang; +} + diff --git a/src/main/java/com/alterdekim/game/entities/Invite.java b/src/main/java/com/alterdekim/game/entities/Invite.java new file mode 100644 index 0000000..b274264 --- /dev/null +++ b/src/main/java/com/alterdekim/game/entities/Invite.java @@ -0,0 +1,23 @@ +package com.alterdekim.game.entities; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "invite_codes") +public class Invite { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable=false) + private String invite_code; +} + diff --git a/src/main/java/com/alterdekim/game/entities/Role.java b/src/main/java/com/alterdekim/game/entities/Role.java new file mode 100644 index 0000000..9cb97da --- /dev/null +++ b/src/main/java/com/alterdekim/game/entities/Role.java @@ -0,0 +1,29 @@ +package com.alterdekim.game.entities; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name="roles") +public class Role +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable=false, unique=true) + private String name; + + @ManyToMany(mappedBy="roles") + private List users; +} + diff --git a/src/main/java/com/alterdekim/game/entities/User.java b/src/main/java/com/alterdekim/game/entities/User.java new file mode 100644 index 0000000..9d7df9d --- /dev/null +++ b/src/main/java/com/alterdekim/game/entities/User.java @@ -0,0 +1,38 @@ +package com.alterdekim.game.entities; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name="users") +public class User { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable=false, unique=true) + private String username; + + @Column(nullable=false) + private String password; + + @ManyToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL) + @JoinTable( + name="users_roles", + joinColumns={@JoinColumn(name="USER_ID", referencedColumnName="ID")}, + inverseJoinColumns={@JoinColumn(name="ROLE_ID", referencedColumnName="ID")}) + private List roles = new ArrayList<>(); + +} \ No newline at end of file diff --git a/src/main/java/com/alterdekim/game/handler/CustomAccessDeniedHandler.java b/src/main/java/com/alterdekim/game/handler/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..6e0f547 --- /dev/null +++ b/src/main/java/com/alterdekim/game/handler/CustomAccessDeniedHandler.java @@ -0,0 +1,16 @@ +package com.alterdekim.game.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + response.sendRedirect("/access-denied"); + } +} + diff --git a/src/main/java/com/alterdekim/game/repository/InviteRepository.java b/src/main/java/com/alterdekim/game/repository/InviteRepository.java new file mode 100644 index 0000000..c3543ef --- /dev/null +++ b/src/main/java/com/alterdekim/game/repository/InviteRepository.java @@ -0,0 +1,11 @@ +package com.alterdekim.game.repository; + +import com.alterdekim.game.entities.Invite; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface InviteRepository extends JpaRepository { + Optional findById(Integer id); +} + diff --git a/src/main/java/com/alterdekim/game/repository/RoleRepository.java b/src/main/java/com/alterdekim/game/repository/RoleRepository.java new file mode 100644 index 0000000..e15c658 --- /dev/null +++ b/src/main/java/com/alterdekim/game/repository/RoleRepository.java @@ -0,0 +1,13 @@ +package com.alterdekim.game.repository; + +import com.alterdekim.game.entities.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface RoleRepository extends JpaRepository { + Role findByName(String name); + + List findAll(); +} + diff --git a/src/main/java/com/alterdekim/game/repository/UserRepository.java b/src/main/java/com/alterdekim/game/repository/UserRepository.java new file mode 100644 index 0000000..78520e2 --- /dev/null +++ b/src/main/java/com/alterdekim/game/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.alterdekim.game.repository; + +import com.alterdekim.game.entities.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + User findByUsername(String username); +} diff --git a/src/main/java/com/alterdekim/game/security/SpringSecurity.java b/src/main/java/com/alterdekim/game/security/SpringSecurity.java new file mode 100644 index 0000000..09c6d50 --- /dev/null +++ b/src/main/java/com/alterdekim/game/security/SpringSecurity.java @@ -0,0 +1,78 @@ +package com.alterdekim.game.security; + +import com.alterdekim.game.handler.CustomAccessDeniedHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +@EnableWebSecurity +public class SpringSecurity { + + @Autowired + private UserDetailsService userDetailsService; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + HttpSessionRequestCache requestCache = new HttpSessionRequestCache(); + requestCache.setMatchingRequestParameterName(null); + http.csrf().disable() + .authorizeHttpRequests((authorize) -> + authorize + .requestMatchers("/game").permitAll() + .requestMatchers("/games").permitAll() + .requestMatchers("/static/**").permitAll() + .requestMatchers("/access-denied").permitAll() + .requestMatchers("/signup").permitAll() + .requestMatchers("/favicon.ico").permitAll() + .requestMatchers("/signup/**").permitAll() + .requestMatchers("/").permitAll() + ).formLogin( + form -> form + .loginPage("/login") + .loginProcessingUrl("/login") + .failureForwardUrl("/") + .defaultSuccessUrl("/game") + .permitAll() + ) + .logout( + logout -> logout + .logoutUrl("/logout") + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .permitAll() + ) + .requestCache((cache) -> cache + .requestCache(requestCache) + ) + .exceptionHandling((exc) -> exc + .accessDeniedHandler(accessDeniedHandler()) + .accessDeniedPage("/access-denied")); + return http.build(); + } + + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return new CustomAccessDeniedHandler(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + + @Bean + public static PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } +} + diff --git a/src/main/java/com/alterdekim/game/service/InviteService.java b/src/main/java/com/alterdekim/game/service/InviteService.java new file mode 100644 index 0000000..682a73c --- /dev/null +++ b/src/main/java/com/alterdekim/game/service/InviteService.java @@ -0,0 +1,7 @@ +package com.alterdekim.game.service; + +import com.alterdekim.game.entities.Invite; + +public interface InviteService { + Invite findById(Integer id); +} diff --git a/src/main/java/com/alterdekim/game/service/InviteServiceImpl.java b/src/main/java/com/alterdekim/game/service/InviteServiceImpl.java new file mode 100644 index 0000000..53c15f3 --- /dev/null +++ b/src/main/java/com/alterdekim/game/service/InviteServiceImpl.java @@ -0,0 +1,20 @@ +package com.alterdekim.game.service; + +import com.alterdekim.game.entities.Invite; +import com.alterdekim.game.repository.InviteRepository; +import org.springframework.stereotype.Service; + +@Service +public class InviteServiceImpl implements InviteService { + + private final InviteRepository repository; + + public InviteServiceImpl(InviteRepository repository) { + this.repository = repository; + } + + @Override + public Invite findById(Integer id) { + return repository.findById(id).orElse(null); + } +} diff --git a/src/main/java/com/alterdekim/game/service/UserService.java b/src/main/java/com/alterdekim/game/service/UserService.java new file mode 100644 index 0000000..559a7bf --- /dev/null +++ b/src/main/java/com/alterdekim/game/service/UserService.java @@ -0,0 +1,15 @@ +package com.alterdekim.game.service; + +import com.alterdekim.game.dto.UserDTO; +import com.alterdekim.game.entities.User; + +import java.util.List; + +public interface UserService { + void saveUser(UserDTO userDto); + + User findByUsername(String usernane); + + List findAllUsers(); +} + diff --git a/src/main/java/com/alterdekim/game/service/UserServiceImpl.java b/src/main/java/com/alterdekim/game/service/UserServiceImpl.java new file mode 100644 index 0000000..98f2d71 --- /dev/null +++ b/src/main/java/com/alterdekim/game/service/UserServiceImpl.java @@ -0,0 +1,68 @@ +package com.alterdekim.game.service; + +import com.alterdekim.game.dto.UserDTO; +import com.alterdekim.game.entities.Role; +import com.alterdekim.game.entities.User; +import com.alterdekim.game.repository.RoleRepository; +import com.alterdekim.game.repository.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + + public UserServiceImpl(UserRepository userRepository, + RoleRepository roleRepository, + PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public void saveUser(UserDTO userDto) { + User user = new User(); + user.setUsername(userDto.getUsername()); + + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + Role role = roleRepository.findByName("ROLE_ADMIN"); + if(role == null){ + role = checkRoleExist(); + } + user.setRoles(Collections.singletonList(role)); + userRepository.save(user); + } + + @Override + public User findByUsername(String username) { + return userRepository.findByUsername(username); + } + + @Override + public List findAllUsers() { + List users = userRepository.findAll(); + return users.stream().map(this::convertEntityToDto) + .collect(Collectors.toList()); + } + + private UserDTO convertEntityToDto(User user){ + UserDTO userDto = new UserDTO(); + userDto.setUsername(user.getUsername()); + return userDto; + } + + private Role checkRoleExist() { + Role role = new Role(); + role.setName("ROLE_ADMIN"); + return roleRepository.save(role); + } +} + diff --git a/src/main/java/com/alterdekim/game/storage/FileSystemStorageService.java b/src/main/java/com/alterdekim/game/storage/FileSystemStorageService.java index c9170f0..0fdabb7 100644 --- a/src/main/java/com/alterdekim/game/storage/FileSystemStorageService.java +++ b/src/main/java/com/alterdekim/game/storage/FileSystemStorageService.java @@ -1,2 +1,106 @@ -package com.alterdekim.game.storage;public class FileSystemStorageService { +package com.alterdekim.game.storage; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.stream.Stream; + +@Service +public class FileSystemStorageService implements StorageService { + + private final Path rootLocation; + + @Autowired + public FileSystemStorageService(StorageProperties properties) { + + if(properties.getLocation().trim().isEmpty()){ + throw new StorageException("File upload location can not be Empty."); + } + + this.rootLocation = Paths.get(new ClassPathResource(properties.getLocation()).getPath()); + } + + @Override + public void store(MultipartFile file) { + try { + if (file.isEmpty()) { + throw new StorageException("Failed to store empty file."); + } + Path destinationFile = this.rootLocation.resolve( + Paths.get(file.getOriginalFilename())) + .normalize().toAbsolutePath(); + if (!destinationFile.getParent().equals(this.rootLocation.toAbsolutePath())) { + throw new StorageException( + "Cannot store file outside current directory."); + } + try (InputStream inputStream = file.getInputStream()) { + Files.copy(inputStream, destinationFile, + StandardCopyOption.REPLACE_EXISTING); + } + } + catch (IOException e) { + throw new StorageException("Failed to store file.", e); + } + } + + @Override + public Stream loadAll() { + try { + return Files.walk(this.rootLocation, 1) + .filter(path -> !path.equals(this.rootLocation)) + .map(this.rootLocation::relativize); + } + catch (IOException e) { + throw new StorageException("Failed to read stored files", e); + } + + } + + @Override + public Path load(String filename) { + return rootLocation.resolve(filename); + } + + @Override + public Resource loadAsResource(String filename) { + try { + Resource resource = new ClassPathResource(filename); + if (resource.exists() || resource.isReadable()) { + return resource; + } + else { + throw new StorageFileNotFoundException( + "Could not read file: " + filename); + + } + } + catch (Exception e) { + throw new StorageFileNotFoundException("Could not read file: " + filename, e); + } + } + + @Override + public void deleteAll() { + FileSystemUtils.deleteRecursively(rootLocation.toFile()); + } + + @Override + public void init() { + try { + Files.createDirectories(rootLocation); + } + catch (IOException e) { + throw new StorageException("Could not initialize storage", e); + } + } } diff --git a/src/main/java/com/alterdekim/game/storage/StorageException.java b/src/main/java/com/alterdekim/game/storage/StorageException.java index be79cba..824209d 100644 --- a/src/main/java/com/alterdekim/game/storage/StorageException.java +++ b/src/main/java/com/alterdekim/game/storage/StorageException.java @@ -1,2 +1,12 @@ -package com.alterdekim.game.storage;public class StorageException { +package com.alterdekim.game.storage; + +public class StorageException extends RuntimeException { + + public StorageException(String message) { + super(message); + } + + public StorageException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/com/alterdekim/game/storage/StorageFileNotFoundException.java b/src/main/java/com/alterdekim/game/storage/StorageFileNotFoundException.java index 32e0c2e..6fa838f 100644 --- a/src/main/java/com/alterdekim/game/storage/StorageFileNotFoundException.java +++ b/src/main/java/com/alterdekim/game/storage/StorageFileNotFoundException.java @@ -1,2 +1,12 @@ -package com.alterdekim.game.storage;public class StorageFileNotFoundException { -} +package com.alterdekim.game.storage; + +public class StorageFileNotFoundException extends StorageException { + + public StorageFileNotFoundException(String message) { + super(message); + } + + public StorageFileNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/alterdekim/game/storage/StorageProperties.java b/src/main/java/com/alterdekim/game/storage/StorageProperties.java index 3d0c3b1..4e51ca1 100644 --- a/src/main/java/com/alterdekim/game/storage/StorageProperties.java +++ b/src/main/java/com/alterdekim/game/storage/StorageProperties.java @@ -1,2 +1,12 @@ -package com.alterdekim.game.storage;public class StorageProperties { +package com.alterdekim.game.storage; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Setter +@Getter +@ConfigurationProperties("storage") +public class StorageProperties { + private String location = "static"; } diff --git a/src/main/java/com/alterdekim/game/storage/StorageService.java b/src/main/java/com/alterdekim/game/storage/StorageService.java index 1f043ae..7a49651 100644 --- a/src/main/java/com/alterdekim/game/storage/StorageService.java +++ b/src/main/java/com/alterdekim/game/storage/StorageService.java @@ -1,2 +1,24 @@ -package com.alterdekim.game.storage;public class StorageService { +package com.alterdekim.game.storage; + +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Path; +import java.util.stream.Stream; + +public interface StorageService { + + void init(); + + void store(MultipartFile file); + + Stream loadAll(); + + Path load(String filename); + + Resource loadAsResource(String filename); + + void deleteAll(); + } + diff --git a/src/main/resources/static/css/game.css b/src/main/resources/static/css/game.css new file mode 100644 index 0000000..09f9198 --- /dev/null +++ b/src/main/resources/static/css/game.css @@ -0,0 +1,506 @@ +body { + font-size: 14px; + font-family: 'Open Sans',tahoma,arial,sans-serif; +} + +._mbtn { + text-align: center; + margin-left: auto; + margin-right: auto; + margin-bottom: auto; + font-size: 12px; +} + +.dropbox { + width: 17%; + position: absolute; + margin-top: 2%; +} + +.dropbox div { + background-color: white; + color: grey; + font-size: 16pt; + padding-top: 5px; + padding-bottom: 5px; +} + +.stars { + position: fixed; + width: 55px; + text-align: center; + display: inline-flex; + margin: auto; + color: white; +} + +._star { + text-align: center; + width: 100%; + margin-top: -10px; + width: 100%; +} + +.hstars { + margin-left: -40px; +} + +._hstar { + margin-top: -50px; + transform: rotate(90deg); +} + +._vstar { + margin-top: -110px; +} + +._hhstar { + margin-left: 60px; +} + +.wrapper { + position: fixed; + top: 0px; + left: 0px; + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + background-color: #262c2f; + overflow: hidden!important; +} +#chatinput { + width: 511px; + height: 24px; + background-color:rgba(0, 0, 0, 0.1); + color: white; + border: none; + outline:none; + font-size:16px; +} +#log { + position: fixed; + overflow-x: hidden; + overflow: scroll; + width: 511px; + height: 480px; + color: white; +} +#loading { + position: fixed; + top: 0px; + left: 0px; + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; + background-color: #262c2f; + overflow: hidden!important; + z-index: 99; +} +.table { + display: grid; + grid-column-gap: 2px; + grid-row-gap: 2px; +} +.corner { + background-color: white; + width: 100px; + height: 100px; +} +.fv { + background-color: #f5f5f5; + width: 100px; + height: 55px; + cursor: pointer; +} +.fh { + background-color: #f5f5f5; + width: 55px; + height: 100px; + cursor: pointer; +} +.cost { + background-color: white; + text-align: center; + color: white; +} + +/* .cost.red { + background-color: #CC0000; + color:#FFFFFF +} + +.cost.green { + background-color: #00CC00; + color:#FFFFFF +} */ + +.up { + margin-top: 18px; +} +.iconH { + width: 55px; + height: 100px; + position:relative; +} +.iconV { + width: 100px; + height: 55px; + position:relative; + /* background-image: url("http://localhost/monopoly/fields/land_rover.png"); */ +} + +.horImg { + /* width:55px; + height:55px; */ + width:55px; + position:absolute; + left:25px; +} + +.vertImg { + /* width:55px; + height:55px; */ + width:55px; + -webkit-transform:rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + transform: rotate(-90deg); + position:absolute; + top:25px; +} + +.rcost { + float: right; + background-color: white; + width: 55px; + height: 18px; + transform: rotate(90deg); + margin-top: 18px; + margin-left: 82px; + text-align: center; + color: white; +} +.lcost { + float: left; + background-color: white; + width: 55px; + height: 18px; + transform: rotate(90deg); + margin-top: 18px; + margin-left: -38px; + text-align: center; + color: white; +} +.table_center { + grid-row-start: 2; + grid-column-start: 2; + grid-column-end: 11; + grid-row-end: 11; +} +.players { + margin-right: 80px; +} +.player { + margin-top: 10px; + background-color: #15191a; + width: 173px; + height: 130px; + color: white; + text-align: center; + font-size: 22px; + padding: 4px; + cursor: pointer; +} +.player-turn { + width: 193px; + height: 150px; +} +.game { + display: inline-flex; +} +/* + chip colors: + red - #f2454a + blue - #00adef + green - #73d249 + purple - #c874ed + orange - #e19d22 +*/ + +.font-red { color: #f2454a; } +.font-blue { color: #00adef; } +.font-green { color: #73d249; } +.font-purple { color: #c874ed; } +.font-orange { color: #e19d22; } + +.red { background-color: #f2454a; } +.blue { background-color: #00adef; } +.green { background-color: #73d249; } +.purple { background-color: #c874ed; } +.orange { background-color: #e19d22; } + + +.chips { + /* background-color:#ff0000; */ + width:100%; + height:100%; + left:173px; + top:0px; + position:absolute; + /* border:1px solid green; */ + + +} + +.chip { + box-shadow: 0 0 10px rgba(0,0,0,1); + border-radius: 50px; + position: absolute; + width: 20px; + height: 20px; + z-index: 98; + grid-row-start: 1; + grid-row-end: 11; + grid-column-start: 1; + grid-column-end: 11; + margin-top: 18px; +} + +.chipMask { + border-radius: 50px; + width: 20px; + height: 20px; + z-index: 99; + background-color: black; + opacity: 0.8; +} + +.btn_string { + display: inline-flex; + padding-left:5px; +} + + +#cell_descr { + z-index:100; + border-radius:10px; + width: 202px; + background-color:#ffffff; + position:absolute; + box-shadow: 0px -5px 10px rgba(10,10,10,0.3); + background-color:#ffffff; + padding-bottom:10px; +} + +.cell_descr_top { + background-color:#646d79; + width:202px; + height:57px; + border-radius:10px 10px 0px 0px; +} + +.rent_table { + color:#646d79; + font-size:11px; + background-color:#ffffff; + margin-left:10px; + margin-right:10px; +} + +.currency { + color:#adb3bb; +} + +.star { + font-size:14px; +} + +.star_big { + color:#fdbd00; + font-size:18px; +} + +.cell_descr_title { + color:#ffffff; + font-size:16px; + width:202px; + padding-left: 10px; + padding-top: 10px; + padding-bottom: 4px; + padding-right: 5px; +} + +.cell_descr_subtitle { + color:#e0e0e0; + font-size:9px; + width:202px; + padding-left: 10px; + padding-top: 0px; + padding-bottom: 5px; + padding-right: 5px; +} + +#dialog { + position: fixed; + width: 491px; + /* height: 200px;*/ + background-color: white; + margin-left:10px; + margin-right:10px; + margin-top:10px; + padding-bottom:10px; + border-radius:10px; + color:#646d79; +} + +#dialog_title { + font-size: 18px; + margin: 10px; +} +#dialog_desc { + font-size: 12px; + margin: 10px; +} + +#contract { + width: 511px; + height: 511px; + background-color: white; + position: fixed; + color: #646d79; + font-size: 20px; + text-align: center; +} + +.btn { + border-radius: 5px; + font-size: 12px; + color: white; + width: 220px; + text-align: center; + padding: 6px 4px; + margin: 6px; + cursor: pointer; +} + +.btn_long { + border-radius: 5px; + font-size: 12px; + color: white; + width: 460px; + text-align: center; + padding: 6px 4px; + margin: 6px; + cursor: pointer; +} + +[contenteditable="true"].single-line { + white-space: nowrap; + width:100px; + overflow: hidden; +} +[contenteditable="true"].single-line br { + display:none; + +} +[contenteditable="true"].single-line * { + display:inline; + white-space:nowrap; +} + +.btn.green, .btn_long.green { + background-color: #00c29e; + +} + +.btn.grey, .btn_long.grey { + background-color: #eceef1; + color:#646d79; +} + +p.contract-player { + margin-top: 0.5em; + margin-bottom: 1em; +} + +.contract_money { + background: transparent; + border: none; + outline: none; + appearance: textfield; + font-size: 20px; + color: #646d79; +} + +h2 { + color: white; + width: 100%; + height: 100%; + margin-top: 50%; + text-align: center; +} + +.diceFace { + position:absolute; + width:200px; + height:200px; + text-align:center; + line-height:200px; + font-size:45px; +} + +.dice_view { + position: absolute; + width:200px; + height:200px; + perspective:600px; + transform: scale(0.5); +} + +.dice { + width:200px; + height:200px; + position:relative; + transform-style:preserve-3d; + transition: transform 1s; +} + +.dice_front { + background-image: url("monopoly/cube/1.jpg"); + background-size: 200px; + transform:rotateY(0deg) translateZ(100px); +} +.dice_right { + background-image: url("monopoly/cube/2.jpg"); + background-size: 200px; + transform:rotateY(90deg) translateZ(100px); +} +.dice_back { + background-image: url("monopoly/cube/3.jpg"); + background-size: 200px; + transform:rotateY(180deg) translateZ(100px); +} +.dice_left { + background-image: url("monopoly/cube/4.jpg"); + background-size: 200px; + transform:rotateY(-90deg) translateZ(100px); +} +.dice_top { + background-image: url("monopoly/cube/5.jpg"); + background-size: 200px; + transform:rotateX(90deg) translateZ(100px); +} +.dice_bottom { + background-image: url("monopoly/cube/6.jpg"); + background-size: 200px; + transform:rotateX(-90deg) translateZ(100px); +} + +.timeout { + position: absolute; + left: 150px; + font-size: 15px; +} \ No newline at end of file diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css new file mode 100644 index 0000000..35ee32e --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,52 @@ +.navbar { + background-color: #fff; +} + +html, body { + font-family: 'Montserrat', sans-serif; + background-color: #f4f4f5; + font-size: 0.875rem; +} + +footer { + bottom: 0; + width: 100%; + background-color: #e6e9ed; +} + + +.nav-item { + margin-right: 1rem; +} + +.nav-item:last-child { + margin-right: 0; +} + +.btn-outline-primary { + border-color: transparent; + color: #5b5d67; +} + +.btn-primary { + background-color: #37bc9d; + color: #fff; + border-color: #37bc9d; +} + +.btn-primary:hover { + background-color: rgba(55,188,157,0.85); + border-color: rgba(55,188,157,0.85); +} + +.btn-outline-primary:hover { + background-color: rgba(55,188,157,0.85); + border-color: rgba(55,188,157,0.85); +} + +.navbar-btn { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.2em; +} \ No newline at end of file diff --git a/src/main/resources/templates/fragments/essentials.html b/src/main/resources/templates/fragments/essentials.html new file mode 100644 index 0000000..f7d9dfc --- /dev/null +++ b/src/main/resources/templates/fragments/essentials.html @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/footer.html b/src/main/resources/templates/fragments/footer.html new file mode 100644 index 0000000..851231a --- /dev/null +++ b/src/main/resources/templates/fragments/footer.html @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/head.html b/src/main/resources/templates/fragments/head.html new file mode 100644 index 0000000..b85308d --- /dev/null +++ b/src/main/resources/templates/fragments/head.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html new file mode 100644 index 0000000..95a5e88 --- /dev/null +++ b/src/main/resources/templates/fragments/navbar.html @@ -0,0 +1,32 @@ + + + + + Nosedive + + + + + + + + Games + + + + + + Friends + + + + + + Inventory + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/game.html b/src/main/resources/templates/game.html new file mode 100644 index 0000000..28f16a7 --- /dev/null +++ b/src/main/resources/templates/game.html @@ -0,0 +1,272 @@ + + + + + + + Монополия онлайн + + + + + + Mitsubishi + 3.000$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Договор + + Вы: + User: + + + + + + + Наличные + + + + + + + + + + Наличные + + + + + + + 0$ + + + Общая сумма + + + 0$ + + + + ПринятьОтказать + + + + + Покупаем? + Lorem ipsum dolor sit amet... + + Купить за 1,000kНа аукцион + + + Купить за 1,000k + + + + + + + Apple + + + + + + + + КупитьПродать + + + + + Продать + + + + + Стройте филиалы, чтобы увеличить ренту + + + Базовая рента + 350k + + + ★ + 1,750k + + + ★ ★ + 5,000k + + + ★ ★ ★ + 11,050k + + + ★ ★ ★ ★ + 13,000k + + + ★ + 15,000k + + + + Стоимость поля + 3,500k + + + Залог поля + 1,750k + + + Выкуп поля + 2,100k + + + Покупка филиала + 2,000k + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..fd2a3f5 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,36 @@ + + + + + + + + + + Login + + + + + + + + + Nickname + + + + Password + + + + Don't have an account? Sign up + + Login + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 0000000..1103390 --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,48 @@ + + + + + + + + + + Sign up + + + + + + + + + Nickname + + + + Password + + + + Invite code + + + + + + + I have read the terms and conditions + + + + Sign up + + Already have an account? Login + + + + + + + + \ No newline at end of file
Договор
Вы:
User:
Покупаем?
Lorem ipsum dolor sit amet...
Don't have an account? Sign up
Already have an account? Login