From e4e2f75f94879156ec14b38d2035e0a581d1922b Mon Sep 17 00:00:00 2001 From: Nico Athanassiadis Date: Thu, 9 Jan 2025 07:46:37 +0100 Subject: [PATCH] SSO Oauth 2 - and other improvements. Single sign on has been added, it works in the same fashion as the Scipro application. Application no longer supports self registration of users. Users can now upload the same file again if they want too. UI SU logotype added, "look and feel" a la stockholm university. UI improved feedback of the uploaded files, now also shows when a file was uploaded together with the status UI improved feedback of the transcribed files, files are grouped in collapsable divs based on the source file. The transcribed files section also shows when the files where created which are based on the upload of the source file, this to help differentiate if the user uploaded the same source file. Bulk download zip file will have a folder structure resembling the grouped structure from the transcribed files section, transcribed files belonging together will be in their correct folder. Increased the varchar limit from the default value to a more appropriate value to handle long file names or paths. When cleaning up files the application now cleans up empty folders when needed. The application needs a bit more internal improvements and polish, for example in some places the application both uses java.io and java.nio it would be better to use java.nio across the board. This can be done at a later date. --- compose.yaml | 13 + pom.xml | 4 + .../seshat/configuration/SecurityConfig.java | 21 +- .../seshat/controllers/FileController.java | 19 +- .../seshat/controllers/LandingController.java | 18 + .../seshat/controllers/LoginController.java | 33 -- .../controllers/RegistrationController.java | 37 -- .../se/su/dsv/seshat/entities/AppUser.java | 18 +- .../su/dsv/seshat/entities/DeletedFile.java | 2 +- .../su/dsv/seshat/entities/FileMetadata.java | 23 +- .../CustomOAuth2loginSuccessHandler.java | 43 ++ .../services/CustomUserDetailService.java | 1 - .../seshat/services/JobProcessorService.java | 25 +- .../dsv/seshat/services/StorageService.java | 55 ++- .../su/dsv/seshat/services/Transcriber.java | 4 +- .../su/dsv/seshat/services/UserService.java | 13 +- src/main/resources/application.properties | 15 +- src/main/resources/static/css/styles.css | 30 +- .../images/SU_logotyp_Landscape_Invert.svg | 379 ++++++++++++++++++ .../resources/templates/file-management.html | 111 +++-- src/main/resources/templates/login.html | 40 -- src/main/resources/templates/register.html | 38 -- 22 files changed, 693 insertions(+), 249 deletions(-) create mode 100644 src/main/java/se/su/dsv/seshat/controllers/LandingController.java delete mode 100644 src/main/java/se/su/dsv/seshat/controllers/LoginController.java delete mode 100644 src/main/java/se/su/dsv/seshat/controllers/RegistrationController.java create mode 100644 src/main/java/se/su/dsv/seshat/services/CustomOAuth2loginSuccessHandler.java create mode 100644 src/main/resources/static/images/SU_logotyp_Landscape_Invert.svg delete mode 100644 src/main/resources/templates/login.html delete mode 100644 src/main/resources/templates/register.html diff --git a/compose.yaml b/compose.yaml index 3c1592d..7cfc102 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,5 +10,18 @@ services: - '3306:3306' volumes: - mariadb_data:/var/lib/mysql + + oauth2: + build: + context: https://github.com/dsv-su/toker.git + dockerfile: embedded.Dockerfile + + ports: + - '51337:8080' + environment: + - CLIENT_ID=seshat + - CLIENT_SECRET=n0tS3cr3t + - CLIENT_REDIRECT_URI=http://localhost:8181/login/oauth2/code/seshat + volumes: mariadb_data: diff --git a/pom.xml b/pom.xml index ad97abb..19f86ce 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,10 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-oauth2-client + org.springframework.boot spring-boot-starter-validation diff --git a/src/main/java/se/su/dsv/seshat/configuration/SecurityConfig.java b/src/main/java/se/su/dsv/seshat/configuration/SecurityConfig.java index 33a2f7f..35a7647 100644 --- a/src/main/java/se/su/dsv/seshat/configuration/SecurityConfig.java +++ b/src/main/java/se/su/dsv/seshat/configuration/SecurityConfig.java @@ -6,24 +6,32 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; +import org.springframework.web.filter.ForwardedHeaderFilter; +import se.su.dsv.seshat.services.CustomOAuth2loginSuccessHandler; @Configuration public class SecurityConfig { @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + public SecurityFilterChain securityFilterChain( + HttpSecurity httpSecurity, + CustomOAuth2loginSuccessHandler customOAuth2loginSuccessHandler + ) throws Exception { httpSecurity.csrf(AbstractHttpConfigurer::disable) + .addFilterBefore(new ForwardedHeaderFilter(), WebAsyncManagerIntegrationFilter.class) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/css/**", "/js/**", "/register", "/login").permitAll() .anyRequest().authenticated() ) - .formLogin(login -> login - .loginPage("/login") - .defaultSuccessUrl("/files/manage", true) - .permitAll() + .oauth2Login(oauth2 -> oauth2 + .successHandler(customOAuth2loginSuccessHandler) ) .logout(logout -> logout - .logoutSuccessUrl("/login?logout") + .logoutSuccessUrl("/") + .invalidateHttpSession(true) + .clearAuthentication(true) + .deleteCookies("JSESSIONID", "auth_code", "refresh_token", "Authorization") .permitAll() ); return httpSecurity.build(); @@ -33,4 +41,5 @@ public class SecurityConfig { public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + } diff --git a/src/main/java/se/su/dsv/seshat/controllers/FileController.java b/src/main/java/se/su/dsv/seshat/controllers/FileController.java index b80a7a2..edee48e 100644 --- a/src/main/java/se/su/dsv/seshat/controllers/FileController.java +++ b/src/main/java/se/su/dsv/seshat/controllers/FileController.java @@ -24,6 +24,8 @@ import se.su.dsv.seshat.services.UserService; import java.io.File; import java.time.LocalDate; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Controller @@ -50,7 +52,19 @@ public class FileController { .filter(file -> file.getJobStatus() != null) .toList(); - model.addAttribute("files", files); + // Map> filesByDirectory = files.stream().collect(Collectors.groupingBy(FileMetadata::getOutputDirectory)); + + Map> filesByDirectory = files.stream() + .collect(Collectors.groupingBy(outputFile -> { + return uploaded.stream() + .filter(uploadedFile -> uploadedFile.getOutputDirectory().equals(outputFile.getOutputDirectory())) + .filter(uploadedFile -> outputFile.getSourceFile().equals(uploadedFile.getFilePath())) + .findFirst() + .orElseThrow(/* Will never happen */); + + })); + + model.addAttribute("filesByDirectory", filesByDirectory); model.addAttribute("statuses", statuses); return "file-management"; @@ -79,7 +93,6 @@ public class FileController { return "redirect:/files/manage"; } - // Browsers do not support DELETE method so for individual file deletion we use GET @GetMapping("files/download/{id}") public ResponseEntity downloadFile(@PathVariable("id") Long id) { try { @@ -116,7 +129,7 @@ public class FileController { } - + // Browsers do not support DELETE method so for individual file deletion we use GET @GetMapping("/files/delete/{id}") public String deleteFile(@PathVariable("id") Long id, Authentication authentication, Model model) { try { diff --git a/src/main/java/se/su/dsv/seshat/controllers/LandingController.java b/src/main/java/se/su/dsv/seshat/controllers/LandingController.java new file mode 100644 index 0000000..f9c712b --- /dev/null +++ b/src/main/java/se/su/dsv/seshat/controllers/LandingController.java @@ -0,0 +1,18 @@ +package se.su.dsv.seshat.controllers; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class LandingController { + + @GetMapping("/") + public String showHomePage(Authentication authentication) { + if (authentication != null) { + return "redirect:/files/manage"; + } + return "redirect:/login"; + } + +} diff --git a/src/main/java/se/su/dsv/seshat/controllers/LoginController.java b/src/main/java/se/su/dsv/seshat/controllers/LoginController.java deleted file mode 100644 index 3fed444..0000000 --- a/src/main/java/se/su/dsv/seshat/controllers/LoginController.java +++ /dev/null @@ -1,33 +0,0 @@ -package se.su.dsv.seshat.controllers; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; - -@Controller -public class LoginController { - - @GetMapping("/") - public String showHomePage(Authentication authentication) { - if (authentication != null) { - return "redirect:/files/manage"; - } - return "redirect:/login"; - } - - @GetMapping("/login") - public String showLoginPage(Model model, String error, String logout) { - if (error != null) { - model.addAttribute("error", "Invalid username or password"); - } - if (logout != null) { - model.addAttribute("message", "Logged out successfully"); - } - return "login"; - } - -} diff --git a/src/main/java/se/su/dsv/seshat/controllers/RegistrationController.java b/src/main/java/se/su/dsv/seshat/controllers/RegistrationController.java deleted file mode 100644 index 4a5e83a..0000000 --- a/src/main/java/se/su/dsv/seshat/controllers/RegistrationController.java +++ /dev/null @@ -1,37 +0,0 @@ -package se.su.dsv.seshat.controllers; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import se.su.dsv.seshat.services.UserService; - -@Controller -public class RegistrationController { - - private final UserService userService; - - public RegistrationController(UserService userService) { - this.userService = userService; - } - - @GetMapping("/register") - public String showRegistrationForm() { - return "register"; - } - - @PostMapping("/register") - public String registerUser(@RequestParam String username, - @RequestParam String email, - @RequestParam String password, - Model model) { - try{ - userService.registerUser(username, email, password); - return "redirect:/login"; - } catch (IllegalArgumentException e) { - model.addAttribute("error", "Registration failed: " + e.getMessage()); - return "register"; - } - } -} diff --git a/src/main/java/se/su/dsv/seshat/entities/AppUser.java b/src/main/java/se/su/dsv/seshat/entities/AppUser.java index 9e775ef..4b02f6b 100644 --- a/src/main/java/se/su/dsv/seshat/entities/AppUser.java +++ b/src/main/java/se/su/dsv/seshat/entities/AppUser.java @@ -25,9 +25,6 @@ public class AppUser { @Column(nullable = false, unique = true) private String username; - @Column(nullable = false) - private String password; - @Column(nullable = false, unique = true) private String email; @@ -42,9 +39,8 @@ public class AppUser { public AppUser() {} - public AppUser(String username, String password, String email, String roles) { + public AppUser(String username, String email, String roles) { this.username = username; - this.password = password; this.email = email; this.roles = roles; } @@ -65,14 +61,6 @@ public class AppUser { this.username = username; } - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - public String getEmail() { return email; } @@ -111,14 +99,13 @@ public class AppUser { if (!(o instanceof AppUser appUser)) return false; return Objects.equals(id, appUser.id) && Objects.equals(username, appUser.username) - && Objects.equals(password, appUser.password) && Objects.equals(email, appUser.email) && Objects.equals(roles, appUser.roles); } @Override public int hashCode() { - return Objects.hash(id, username, password, email, roles); + return Objects.hash(id, username, email, roles); } @Override @@ -126,7 +113,6 @@ public class AppUser { return "AppUser{" + "id=" + id + ", username='" + username + '\'' + - ", password='" + password + '\'' + ", email='" + email + '\'' + ", roles='" + roles + '\'' + ", createdAt=" + createdAt + diff --git a/src/main/java/se/su/dsv/seshat/entities/DeletedFile.java b/src/main/java/se/su/dsv/seshat/entities/DeletedFile.java index 22841fa..4e8db25 100644 --- a/src/main/java/se/su/dsv/seshat/entities/DeletedFile.java +++ b/src/main/java/se/su/dsv/seshat/entities/DeletedFile.java @@ -15,7 +15,7 @@ public class DeletedFile { @GeneratedValue private Long id; - @Column(nullable = false) + @Column(nullable = false, length = 1000) private String filePath; @Column(nullable = false) diff --git a/src/main/java/se/su/dsv/seshat/entities/FileMetadata.java b/src/main/java/se/su/dsv/seshat/entities/FileMetadata.java index e7e099c..d0c5e06 100644 --- a/src/main/java/se/su/dsv/seshat/entities/FileMetadata.java +++ b/src/main/java/se/su/dsv/seshat/entities/FileMetadata.java @@ -23,12 +23,15 @@ public class FileMetadata { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(nullable = false, length = 512) private String fileName; - @Column(nullable = false) + @Column(nullable = false, length = 1000) private String filePath; + @Column(length = 1000) + private String sourceFile; + private String language; @ManyToOne(fetch = FetchType.EAGER) @@ -36,13 +39,13 @@ public class FileMetadata { private AppUser user; @Column(nullable = false) - private LocalDateTime uploadedAt = LocalDateTime.now(); + private LocalDateTime uploadedAt; @Enumerated(EnumType.STRING) - @Column(name ="job_status", nullable = false, length = 20) + @Column(name ="job_status", length = 20) private JobStatus jobStatus = JobStatus.PENDING; - @Column + @Column(length = 1000) private String outputDirectory; public FileMetadata() {} @@ -76,6 +79,14 @@ public class FileMetadata { this.filePath = filePath; } + public String getSourceFile() { + return sourceFile; + } + + public void setSourceFile(String sourceFile) { + this.sourceFile = sourceFile; + } + public String getLanguage() { return language; } @@ -116,6 +127,8 @@ public class FileMetadata { this.outputDirectory = outputDirectory; } + + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/se/su/dsv/seshat/services/CustomOAuth2loginSuccessHandler.java b/src/main/java/se/su/dsv/seshat/services/CustomOAuth2loginSuccessHandler.java new file mode 100644 index 0000000..b1cb137 --- /dev/null +++ b/src/main/java/se/su/dsv/seshat/services/CustomOAuth2loginSuccessHandler.java @@ -0,0 +1,43 @@ +package se.su.dsv.seshat.services; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Service +public class CustomOAuth2loginSuccessHandler implements AuthenticationSuccessHandler { + private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2loginSuccessHandler.class); + + private final String redirectUrl; + private final UserService userService; + + public CustomOAuth2loginSuccessHandler(@Value("${app.onSuccess-homepage}") String redirectUrl, UserService userService) { + this.redirectUrl = redirectUrl; + this.userService = userService; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + logger.info("OAuth2 attributes: {}", oAuth2User.getAttributes()); + + String username = oAuth2User.getName(); + // If the user does not have an email, set it to "no-email". We will not send any eamil notifications to this user. + String email = oAuth2User.getAttribute("mail") != null ? oAuth2User.getAttribute("mail") : "no-email"; + + + if(!userService.existsByUsername(oAuth2User.getAttribute("principal"))) { + userService.registerUser(username, email); + } + response.sendRedirect(redirectUrl); + } +} diff --git a/src/main/java/se/su/dsv/seshat/services/CustomUserDetailService.java b/src/main/java/se/su/dsv/seshat/services/CustomUserDetailService.java index d377fbe..2a49956 100644 --- a/src/main/java/se/su/dsv/seshat/services/CustomUserDetailService.java +++ b/src/main/java/se/su/dsv/seshat/services/CustomUserDetailService.java @@ -23,7 +23,6 @@ public class CustomUserDetailService implements UserDetailsService { .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); return User.builder() .username(appUser.getUsername()) - .password(appUser.getPassword()) .roles(appUser.getRoles().split(",")) .build(); } diff --git a/src/main/java/se/su/dsv/seshat/services/JobProcessorService.java b/src/main/java/se/su/dsv/seshat/services/JobProcessorService.java index a9c9bd1..9408433 100644 --- a/src/main/java/se/su/dsv/seshat/services/JobProcessorService.java +++ b/src/main/java/se/su/dsv/seshat/services/JobProcessorService.java @@ -3,7 +3,8 @@ package se.su.dsv.seshat.services; import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; -import org.springframework.beans.factory.annotation.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Service; import se.su.dsv.seshat.entities.DeletedFile; @@ -15,10 +16,9 @@ import se.su.dsv.seshat.repositories.FileMetadataRepository; import java.io.File; import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Service public class JobProcessorService { @@ -31,9 +31,6 @@ public class JobProcessorService { private final StorageService storageService; private final DeletedFileRepository deletedFileRepository; - @Value("${app.output-root}") - private String outputRoot; - public JobProcessorService(FileMetadataRepository fileMetadataRepository, Transcriber transcriber, StorageService storageService, @@ -88,9 +85,11 @@ public class JobProcessorService { if (transcribe) { logger.info("Transcription successful for file: {}", managedJob.getFileName()); - storageService.addTranscribedFilesToDatabase(managedJob.getUser(), managedJob.getOutputDirectory()); + managedJob.setJobStatus(JobStatus.COMPLETED); + fileMetadataRepository.saveAndFlush(managedJob); + //TODO: This method can just take the filemetadata object managedJob as arugument we will need further information from it. + storageService.addTranscribedFilesToDatabase(managedJob); cleanupFile(managedJob); - fileMetadataRepository.delete(managedJob); // Delete the job after successful transcription break; } else { logger.info("Transcription failed for file: {}", managedJob.getFileName()); @@ -111,20 +110,22 @@ public class JobProcessorService { } - private boolean cleanupFile(FileMetadata jobFile) { + private void cleanupFile(FileMetadata jobFile) { String filePath = jobFile.getFilePath(); File file = new File(filePath); + File parentDir = file.getParentFile(); if(file.exists()) { if(file.delete()) { recordFileDeletion(filePath, "JobProcessorService"); + // Delete the parent directory if it is empty + if(parentDir != null && parentDir.isDirectory() && Objects.requireNonNull(parentDir.list()).length == 0) { + parentDir.delete(); + } logger.info("File deleted successfully: {}", filePath); - return true; } else { logger.error("Failed to delete file: {}", filePath); - return false; } } - return false; } private void recordFileDeletion(String filePath, String jobProcessorService) { diff --git a/src/main/java/se/su/dsv/seshat/services/StorageService.java b/src/main/java/se/su/dsv/seshat/services/StorageService.java index b57bdec..4971a38 100644 --- a/src/main/java/se/su/dsv/seshat/services/StorageService.java +++ b/src/main/java/se/su/dsv/seshat/services/StorageService.java @@ -1,6 +1,8 @@ package se.su.dsv.seshat.services; import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -18,7 +20,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -26,6 +31,8 @@ import java.util.zip.ZipOutputStream; @Service public class StorageService { + private final static Logger logger = LoggerFactory.getLogger(StorageService.class); + private final DeletedFileRepository deletedFileRepository; @Value("${app.upload-root}") private String uploadRoot; @@ -41,7 +48,9 @@ public class StorageService { } @Transactional - public void addTranscribedFilesToDatabase(AppUser user, String outputDirectory) { + public void addTranscribedFilesToDatabase(FileMetadata sourceFile) { + String outputDirectory = sourceFile.getOutputDirectory(); + AppUser user = sourceFile.getUser(); File userOutputDirectory = new File(outputDirectory); if(!userOutputDirectory.exists() || !userOutputDirectory.isDirectory()) { return; @@ -64,6 +73,9 @@ public class StorageService { fileMetadata.setFilePath(filePath); fileMetadata.setUser(user); fileMetadata.setJobStatus(null); + fileMetadata.setUploadedAt(sourceFile.getUploadedAt()); + fileMetadata.setOutputDirectory(sourceFile.getOutputDirectory()); + fileMetadata.setSourceFile(sourceFile.getFilePath()); fileMetadataRepository.save(fileMetadata); } } @@ -84,7 +96,7 @@ public class StorageService { String sanitizedFilename = sanitizeFilename(originalFilename); // Users upload directory - Path userUploadDir = Paths.get(uploadRoot, user.getUsername()); + Path userUploadDir = Paths.get(uploadRoot, user.getUsername(), UUID.randomUUID().toString()); try { if(!Files.exists(userUploadDir)) { @@ -107,7 +119,9 @@ public class StorageService { } else { metadata.setLanguage("auto"); } - metadata.setOutputDirectory(outputRoot + File.separator + user.getUsername()); + metadata.setUploadedAt(LocalDateTime.now()); + String fileFolder = fileNameAndUploadedTime(metadata); + metadata.setOutputDirectory(outputRoot + File.separator + user.getUsername() + File.separator + fileFolder); metadata.setUser(user); return fileMetadataRepository.save(metadata); } catch (IOException e) { @@ -126,6 +140,7 @@ public class StorageService { return fileMetadataRepository.findByUserId(user.getId()) .stream() .filter(file -> file.getFilePath().startsWith(uploadRoot)) + .sorted((file1, file2) -> file2.getUploadedAt().compareTo(file1.getUploadedAt())) .collect(Collectors.toList()); } @@ -147,9 +162,21 @@ public class StorageService { deletedFile.setDeletionTime(LocalDateTime.now()); deletedFileRepository.save(deletedFile); fileMetadataRepository.delete(fileMetadata); + + // Delete the directory if it is empty + Path outputDirectory = Paths.get(fileMetadata.getOutputDirectory()); + if (Files.exists(outputDirectory) && Files.isDirectory(outputDirectory)) { + File[] files = outputDirectory.toFile().listFiles(); + if (files != null && files.length == 0) { + Files.delete(outputDirectory); + } + } + return true; } catch (IllegalArgumentException e) { return false; + } catch (IOException e) { + logger.info("Failed to delete file or directory: " + e.getMessage()); } } else { return false; @@ -166,6 +193,9 @@ public class StorageService { } public File createZipFromFiles(List fileIds, AppUser user) throws IOException { + // TODO: Use java.nio FileSystems.newFileSystem(Files.createTempFile("selected_files", ".zip")); + // FileSystems.newFileSystem(Files.createTempFile("selected_files", ".zip")); + // Temporary ZIP file File zipFile = File.createTempFile("selected_files", ".zip"); @@ -183,9 +213,9 @@ public class StorageService { File file = new File(fileMetadata.getFilePath()); if (file.exists()) { Path filePath = file.toPath(); - + String closestParentDir = filePath.getName(filePath.getNameCount() - 2).toString(); // Add new entry for the file in the ZIP - zos.putNextEntry(new ZipEntry(file.getName())); + zos.putNextEntry(new ZipEntry(closestParentDir + File.separator + file.getName())); // Copy file contents to the ZIP Files.copy(filePath, zos); @@ -200,4 +230,19 @@ public class StorageService { private String sanitizeFilename(String filename) { return filename.replaceAll("[^a-zA-Z0-9.-]", "_"); } + + private String fileNameAndUploadedTime(FileMetadata file) { + String fileName = file.getFileName(); + LocalDateTime uploadedAt = file.getUploadedAt(); + String uploadedToSeconds = uploadedAt.truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + return fileNameWithoutExtension(file.getFileName()) + "_" + uploadedToSeconds.replaceAll(":|-", ""); + } + + private String fileNameWithoutExtension(String fileName) { + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex != -1) { + return fileName.substring(0, lastDotIndex); + } + return fileName; + } } diff --git a/src/main/java/se/su/dsv/seshat/services/Transcriber.java b/src/main/java/se/su/dsv/seshat/services/Transcriber.java index 9d7b7e5..f7fe21e 100644 --- a/src/main/java/se/su/dsv/seshat/services/Transcriber.java +++ b/src/main/java/se/su/dsv/seshat/services/Transcriber.java @@ -1,11 +1,11 @@ package se.su.dsv.seshat.services; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Service public class Transcriber { diff --git a/src/main/java/se/su/dsv/seshat/services/UserService.java b/src/main/java/se/su/dsv/seshat/services/UserService.java index c8e2ef7..e54895c 100644 --- a/src/main/java/se/su/dsv/seshat/services/UserService.java +++ b/src/main/java/se/su/dsv/seshat/services/UserService.java @@ -1,6 +1,5 @@ package se.su.dsv.seshat.services; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import se.su.dsv.seshat.entities.AppUser; import se.su.dsv.seshat.repositories.AppUserRepository; @@ -9,14 +8,12 @@ import se.su.dsv.seshat.repositories.AppUserRepository; public class UserService { private final AppUserRepository appUserRepository; - private final PasswordEncoder passwordEncoder; - public UserService(AppUserRepository appUserRepository, PasswordEncoder passwordEncoder) { + public UserService(AppUserRepository appUserRepository) { this.appUserRepository = appUserRepository; - this.passwordEncoder = passwordEncoder; } - public void registerUser(String username, String email,String password) { + public void registerUser(String username, String email) { if (appUserRepository.existsByUsername(username)) { throw new IllegalArgumentException("Username already exists"); } @@ -24,7 +21,7 @@ public class UserService { throw new IllegalArgumentException("Email already exists"); } - AppUser newUser = new AppUser(username, passwordEncoder.encode(password), email, "USER"); + AppUser newUser = new AppUser(username, email, "USER"); appUserRepository.save(newUser); } @@ -32,4 +29,8 @@ public class UserService { return appUserRepository.findByUsername(username) .orElseThrow(() -> new IllegalArgumentException("User not found")); } + + public boolean existsByUsername(String username) { + return appUserRepository.existsByUsername(username); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 96825b1..2710d11 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,7 +6,7 @@ spring.servlet.multipart.max-request-size=5GB app.upload-root=/seshat/uploads app.output-root=/seshat/outputs -app.api-url=http://localhost:8181/seshat/api +app.onSuccess-homepage=/files/manage # Database properties (local development) spring.datasource.url=jdbc:mariadb://localhost:3306/seshat @@ -16,4 +16,15 @@ spring.datasource.driver-class-name=org.mariadb.jdbc.Driver # JPA properties spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=false \ No newline at end of file +spring.jpa.show-sql=false + +# OAuth2 properties, remember if you change the registration.provider the provider properties must be updated +spring.security.oauth2.client.provider.docker.authorization-uri=http://localhost:51337/authorize +spring.security.oauth2.client.provider.docker.token-uri=http://localhost:51337/exchange +spring.security.oauth2.client.provider.docker.user-info-uri=http://localhost:51337/verify +spring.security.oauth2.client.provider.docker.user-name-attribute=sub +spring.security.oauth2.client.registration.seshat.client-id=seshat +spring.security.oauth2.client.registration.seshat.client-secret=n0tS3cr3t +spring.security.oauth2.client.registration.seshat.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.seshat.provider=docker +spring.security.oauth2.client.registration.seshat.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} \ No newline at end of file diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index f941637..4f81baa 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -1,3 +1,7 @@ +:root { + --bs-primary-rgb: 0, 47, 95; + --bs-btn-bg: #002F5F; +} /* Sticky footer layout */ /* Header Styling */ .header { @@ -7,8 +11,9 @@ } .header .app-title { - font-size: 1.5rem; - font-weight: bold; + font-family: 'PMN Caecilia', serif; + font-size: 2rem; + font-weight: normal; margin: 0; } @@ -23,8 +28,27 @@ font-size: 1.5rem; } +.logo { + width: 12rem; + margin-right: 1rem; +} + +.table-wrapper { + max-height: 160px; +} + +.table thead th { + position: sticky; + top: 0; + z-index: 1; + background-color: #f9fafb; +} +footer { + margin-top: 1rem; +} + html, body { - height: 100%; /* Ensure the height of the body is at least the viewport height */ + min-height: 100dvh; } body { diff --git a/src/main/resources/static/images/SU_logotyp_Landscape_Invert.svg b/src/main/resources/static/images/SU_logotyp_Landscape_Invert.svg new file mode 100644 index 0000000..8cd8db8 --- /dev/null +++ b/src/main/resources/static/images/SU_logotyp_Landscape_Invert.svg @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/templates/file-management.html b/src/main/resources/templates/file-management.html index 2121b01..ff54067 100644 --- a/src/main/resources/templates/file-management.html +++ b/src/main/resources/templates/file-management.html @@ -5,17 +5,20 @@ File Management - Seshat App - +
+

Seshat Audio Transcriber