diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..d4e98287 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +java openjdk-19.0.2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..ad73c17c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM maven:3.9.2-eclipse-temurin-20 AS build + + +# CMD mvn spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=prod,--server.port=$PORT" + + +# +# Build stage +# + +COPY . . +RUN mvn clean package -DskipTests + +# +# Package stage +# +# FROM openjdk:20-jdk-slim +# COPY --from=build ./target/typoreporter-*.jar typoreporter.jar + +CMD java -Xmx256m -jar target/typoreporter-*.jar --spring.profiles.active=default,prod --server.port=$PORT diff --git a/docs/requirements.yml b/docs/requirements.yml new file mode 100644 index 00000000..1a535353 --- /dev/null +++ b/docs/requirements.yml @@ -0,0 +1,26 @@ +--- +functional: + - Пользователь должен иметь возможность зарегестрироваться в приложении. + - Пользователь должен иметь возможность добавить скрипт на свой сайт. + - На сервисе, в личном аккаунте пользователя, должны отображаться ошибки, которые нашли на сайтах пользователя. + - Пользователь должен иметь возможность отслеживать статус ошибок, найденных на своем сайте, через сервис. #отправлено, в работе,решено, отменено, всего + - Пользователь должен иметь возможность корректировать ошибки через сервис. + - Пользователь должен иметь возможность просматривать пространства, которые он создал. + - Пользователь не должен иметь возможность просматривать пространства, созданные другими пользователями. + - Пользователь должен иметь возможность дать имя пространству. + - Пользователь должен иметь возможность описать пространство. + - Пользователь должен иметь возможность добавлять пользователей в созданное пространство. + - Должна быть возможность просматривать информацию об аккаунте. + - Должна быть возможность изменять информацию об аккаунте #ФИО,почту, пароль +non-functional: + - Сервис должен быть разработан с учетом удобства использования и оптимизирован для скорости работы. + - Сервис должен быть доступен на различных устройствах. + - Сервис должен иметь мобильную версию. + - Сервис должен быть безопасным и защищать данные пользователя. + - Сервис должен быть протестирован на совместимость с последними версиями браузеров. + - Сервис должен должен быть способен обрабатывать большое количество трафика. + - Информация на сайте должна быть представлена на русском и английском языках. +implicit: + - Сервис содержит функцию "теста" #новый пользователь мог бы иметь возможность сам себе отправить ошибку и посмотреть как работает сервис + - Сервис содержит инструкцию по установке скрипта на свой сайт + - Сервис содержит инструкцию по работе в личном кабинете пользователя diff --git a/pom.xml b/pom.xml index 08b1e839..a965c1da 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,11 @@ org.springframework.boot spring-boot-starter-thymeleaf + + nz.net.ultraq.thymeleaf + thymeleaf-layout-dialect + 3.2.1 + org.webjars diff --git a/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java b/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java index 306d13c4..b3d4a031 100644 --- a/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java +++ b/src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java @@ -19,6 +19,10 @@ import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.List; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; @@ -73,7 +77,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, http.exceptionHandling().accessDeniedHandler(accessDeniedHandler()); http.authorizeHttpRequests(authz -> authz - .requestMatchers(GET, "/webjars/**", "/widget/**", "/fragments/**", "/img/**").permitAll() + .requestMatchers(GET, "/webjars/**", "/widget/**", "/fragments/**", "/css/**", "/img/**").permitAll() .requestMatchers("/", "/login", "/signup", "/error").permitAll() .anyRequest().authenticated() ) @@ -99,4 +103,17 @@ public SecurityFilterChain filterChain(HttpSecurity http, public AccessDeniedHandler accessDeniedHandler() { return new CustomAccessDeniedHandler(); } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + // TODO: allow sending a request only from the pages specified in the Workspace settings + configuration.addAllowedOriginPattern("*"); + configuration.addAllowedHeader("*"); + configuration.setAllowCredentials(true); + configuration.setAllowedMethods(List.of("POST", "GET")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } diff --git a/src/main/java/io/hexlet/typoreporter/domain/account/Account.java b/src/main/java/io/hexlet/typoreporter/domain/account/Account.java index 9a4c3cc1..66b9ecc9 100644 --- a/src/main/java/io/hexlet/typoreporter/domain/account/Account.java +++ b/src/main/java/io/hexlet/typoreporter/domain/account/Account.java @@ -107,6 +107,11 @@ public Account addWorkspaceRole(WorkspaceRole workspaceRole) { return this; } + public void removeWorkSpaceRole(WorkspaceRole workspaceRole) { + workspaceRoles.remove(workspaceRole); + workspaceRole.setAccount(null); + } + @Override public boolean equals(Object o) { return this == o || id != null && o instanceof Account other && id.equals(other.id); diff --git a/src/main/java/io/hexlet/typoreporter/domain/workspace/Workspace.java b/src/main/java/io/hexlet/typoreporter/domain/workspace/Workspace.java index 59444ad6..01ea2348 100644 --- a/src/main/java/io/hexlet/typoreporter/domain/workspace/Workspace.java +++ b/src/main/java/io/hexlet/typoreporter/domain/workspace/Workspace.java @@ -85,6 +85,11 @@ public Workspace addWorkspaceRole(WorkspaceRole workspaceRole) { return this; } + public void removeWorkSpaceRole(WorkspaceRole workspaceRole) { + workspaceRoles.remove(workspaceRole); + workspaceRole.setWorkspace(null); + } + @Override public int hashCode() { return getClass().hashCode(); diff --git a/src/main/java/io/hexlet/typoreporter/repository/WorkspaceRoleRepository.java b/src/main/java/io/hexlet/typoreporter/repository/WorkspaceRoleRepository.java index 6962009a..e5d56f7f 100644 --- a/src/main/java/io/hexlet/typoreporter/repository/WorkspaceRoleRepository.java +++ b/src/main/java/io/hexlet/typoreporter/repository/WorkspaceRoleRepository.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface WorkspaceRoleRepository extends JpaRepository { @@ -18,4 +19,7 @@ public interface WorkspaceRoleRepository extends JpaRepository getWorkspaceRolesByWorkspaceId(Long workspaceId); + + @EntityGraph(attributePaths = {"account", "workspace"}) + Optional getWorkspaceRoleByAccountIdAndWorkspaceId(Long accountId, Long workspaceId); } diff --git a/src/main/java/io/hexlet/typoreporter/service/AccountService.java b/src/main/java/io/hexlet/typoreporter/service/AccountService.java index 141e3fac..49ad5333 100644 --- a/src/main/java/io/hexlet/typoreporter/service/AccountService.java +++ b/src/main/java/io/hexlet/typoreporter/service/AccountService.java @@ -15,6 +15,7 @@ import io.hexlet.typoreporter.service.mapper.AccountMapper; import io.hexlet.typoreporter.service.mapper.WorkspaceRoleMapper; import io.hexlet.typoreporter.web.exception.AccountAlreadyExistException; +import io.hexlet.typoreporter.web.exception.AccountNotFoundException; import io.hexlet.typoreporter.web.exception.NewPasswordTheSameException; import io.hexlet.typoreporter.web.exception.OldPasswordWrongException; import lombok.RequiredArgsConstructor; @@ -87,6 +88,17 @@ public Optional getUpdateProfile(final String name) { .map(accountMapper::toUpdateProfile); } + @Transactional(readOnly = true) + public List findAll() { + return accountRepository.findAll(); + } + + @Transactional(readOnly = true) + public Account findByUsername(String userName) { + return accountRepository.findAccountByUsername(userName) + .orElseThrow(() -> new AccountNotFoundException(userName)); + } + public Optional updateProfile(final UpdateProfile updateProfile, final String name) { final var sourceAccount = accountRepository.findAccountByUsername(name); diff --git a/src/main/java/io/hexlet/typoreporter/service/WorkspaceRoleService.java b/src/main/java/io/hexlet/typoreporter/service/WorkspaceRoleService.java index 11dcb586..09773e11 100644 --- a/src/main/java/io/hexlet/typoreporter/service/WorkspaceRoleService.java +++ b/src/main/java/io/hexlet/typoreporter/service/WorkspaceRoleService.java @@ -9,6 +9,7 @@ import io.hexlet.typoreporter.repository.WorkspaceRoleRepository; import io.hexlet.typoreporter.web.exception.AccountNotFoundException; import io.hexlet.typoreporter.web.exception.WorkspaceNotFoundException; +import io.hexlet.typoreporter.web.exception.WorkspaceRoleNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,7 +20,7 @@ @RequiredArgsConstructor public class WorkspaceRoleService { - private final WorkspaceRoleRepository repository; + private final WorkspaceRoleRepository workspaceRoleRepository; private final WorkspaceRepository workspaceRepository; @@ -41,6 +42,23 @@ public WorkspaceRole addAccountToWorkspace(String wksName, String accEmail) { workspaceRepository.getReferenceById(wksId), accountRepository.getReferenceById(accId) ); - return repository.save(workspaceRole); + return workspaceRoleRepository.save(workspaceRole); } + + @Transactional + public void deleteAccountFromWorkspace(String workspaceName, String accountEmail) { + final var account = accountRepository.findAccountByEmail(accountEmail) + .orElseThrow(() -> new AccountNotFoundException(accountEmail)); + final var workspace = workspaceRepository.getWorkspaceByName(workspaceName) + .orElseThrow(() -> new WorkspaceNotFoundException(workspaceName)); + final var beingDeleteRole = workspaceRoleRepository.getWorkspaceRoleByAccountIdAndWorkspaceId( + account.getId(), + workspace.getId()) + .orElseThrow(() -> new WorkspaceRoleNotFoundException(account.getId(), workspace.getId())); + account.removeWorkSpaceRole(beingDeleteRole); + workspace.removeWorkSpaceRole(beingDeleteRole); + accountRepository.save(account); + workspaceRepository.save(workspace); + } + } diff --git a/src/main/java/io/hexlet/typoreporter/service/WorkspaceService.java b/src/main/java/io/hexlet/typoreporter/service/WorkspaceService.java index 740dfa10..aa5e5769 100644 --- a/src/main/java/io/hexlet/typoreporter/service/WorkspaceService.java +++ b/src/main/java/io/hexlet/typoreporter/service/WorkspaceService.java @@ -8,11 +8,14 @@ import io.hexlet.typoreporter.domain.workspacesettings.WorkspaceSettings; import io.hexlet.typoreporter.repository.AccountRepository; import io.hexlet.typoreporter.repository.WorkspaceRepository; +import io.hexlet.typoreporter.repository.WorkspaceRoleRepository; import io.hexlet.typoreporter.repository.WorkspaceSettingsRepository; import io.hexlet.typoreporter.service.dto.workspace.CreateWorkspace; import io.hexlet.typoreporter.service.dto.workspace.WorkspaceInfo; import io.hexlet.typoreporter.service.mapper.WorkspaceMapper; +import io.hexlet.typoreporter.web.exception.AccountNotFoundException; import io.hexlet.typoreporter.web.exception.WorkspaceAlreadyExistException; +import io.hexlet.typoreporter.web.exception.WorkspaceNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,6 +38,7 @@ public class WorkspaceService { private final WorkspaceMapper workspaceMapper; private final AccountRepository accountRepository; + private final WorkspaceRoleRepository workspaceRoleRepository; @Transactional(readOnly = true) public List getAllWorkspacesInfo() { @@ -124,4 +128,17 @@ public boolean isUserRelatedToWorkspace(String wksName, String username) { .anyMatch(wks -> wks.getName().equals(wksName))) .orElse(false); } + + @Transactional(readOnly = true) + public boolean isAdminRoleUserInWorkspace(String wksName, String username) { + final var account = accountRepository.findAccountByUsername(username). + orElseThrow(() -> new AccountNotFoundException(username)); + final var workspace = repository.getWorkspaceByName(wksName). + orElseThrow(() -> new WorkspaceNotFoundException(wksName)); + final var workSpaceRoleOptional = workspaceRoleRepository.getWorkspaceRoleByAccountIdAndWorkspaceId( + account.getId(), + workspace.getId() + ); + return workSpaceRoleOptional.filter(workspaceRole -> workspaceRole.getRole() == AccountRole.ROLE_ADMIN).isPresent(); + } } diff --git a/src/main/java/io/hexlet/typoreporter/web/WorkspaceApi.java b/src/main/java/io/hexlet/typoreporter/web/WorkspaceApi.java index 42894ed5..6826e87b 100644 --- a/src/main/java/io/hexlet/typoreporter/web/WorkspaceApi.java +++ b/src/main/java/io/hexlet/typoreporter/web/WorkspaceApi.java @@ -26,11 +26,6 @@ public class WorkspaceApi { private final TypoService service; @PostMapping("/{id}/typos") - // TODO: allow sending a request only from the pages specified in the Workspace settings - @CrossOrigin( - originPatterns = {"*"}, - allowCredentials = "true" - ) public ResponseEntity addTypoReport(@PathVariable long id, Authentication authentication, @Valid @RequestBody TypoReport typoReport, diff --git a/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java b/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java index a91419a2..151ee0c4 100644 --- a/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java +++ b/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java @@ -3,16 +3,17 @@ import io.hexlet.typoreporter.domain.account.Account; import io.hexlet.typoreporter.domain.workspace.Workspace; import io.hexlet.typoreporter.domain.workspace.WorkspaceRole; -import io.hexlet.typoreporter.repository.AccountRepository; -import io.hexlet.typoreporter.repository.WorkspaceRepository; +import io.hexlet.typoreporter.service.AccountService; import io.hexlet.typoreporter.service.TypoService; import io.hexlet.typoreporter.service.WorkspaceRoleService; import io.hexlet.typoreporter.service.WorkspaceService; import io.hexlet.typoreporter.service.dto.typo.TypoInfo; import io.hexlet.typoreporter.service.dto.workspace.CreateWorkspace; +import io.hexlet.typoreporter.service.dto.workspace.WorkspaceInfo; import io.hexlet.typoreporter.web.exception.AccountNotFoundException; import io.hexlet.typoreporter.web.exception.WorkspaceAlreadyExistException; import io.hexlet.typoreporter.web.exception.WorkspaceNotFoundException; +import io.hexlet.typoreporter.web.exception.WorkspaceRoleNotFoundException; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,6 +25,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.SortDefault; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -37,7 +40,8 @@ import org.springframework.web.bind.annotation.RequestParam; import java.security.Principal; -import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; @@ -56,6 +60,8 @@ public class WorkspaceController { private static final String IS_USER_RELATED_TO_WKS = "@workspaceService.isUserRelatedToWorkspace(#wksName, authentication.name)"; + private static final String IS_USER_ADMIN_IN_WKS = + "@workspaceService.isAdminRoleUserInWorkspace(#wksName, authentication.name)"; private final TreeSet availableSizes = new TreeSet<>(List.of(2, 5, 10, 15, 25)); @@ -63,9 +69,7 @@ public class WorkspaceController { private final WorkspaceService workspaceService; - private final AccountRepository accountRepository; - - private final WorkspaceRepository workspaceRepository; + private final AccountService accountService; private final WorkspaceRoleService workspaceRoleService; @@ -213,45 +217,37 @@ public String getWorkspaceUsersPage(Model model, @PathVariable String wksName, @SortDefault("createdDate") Pageable pageable) { - var wksOptional = workspaceService.getWorkspaceInfoByName(wksName); - if (wksOptional.isEmpty()) { + Optional workSpaceInfoOptional = workspaceService.getWorkspaceInfoByName(wksName); + if (workSpaceInfoOptional.isEmpty()) { //TODO send error page log.error("Workspace with name {} not found", wksName); return "redirect:/workspaces"; } model.addAttribute("wksName", wksName); - model.addAttribute("wksInfo", wksOptional.get()); + model.addAttribute("wksInfo", workSpaceInfoOptional.get()); getStatisticDataToModel(model, wksName); getLastTypoDataToModel(model, wksName); - Optional workspaceOptional = workspaceRepository.getWorkspaceByName(wksName); - if (workspaceOptional.isEmpty()) { - //TODO send error page - log.error("Workspace with name {} not found", wksName); - return "redirect:/workspaces"; - } - - Set workspaces = workspaceOptional.get().getWorkspaceRoles(); - List accounts = new ArrayList<>(); - if (!workspaces.isEmpty()) { - accounts = workspaces.stream() - .map(a -> a.getAccount()) - .collect(Collectors.toList()); - } - - var size = Optional.ofNullable(availableSizes.floor(pageable.getPageSize())).orElseGet(availableSizes::first); - var pageRequest = PageRequest.of(pageable.getPageNumber(), size, pageable.getSort()); - Page userPage = new PageImpl<>(accounts, pageable, accounts.size()); - + Optional workspaceOptional = workspaceService.getWorkspaceByName(wksName); + Set workspaceRoles = workspaceOptional.get().getWorkspaceRoles(); + List linkedAccounts = workspaceRoles.stream() + .map(WorkspaceRole::getAccount) + .collect(Collectors.toList()); + List allAccounts = accountService.findAll(); + List nonLinkedAccounts = getNonLinkedAccounts(allAccounts, linkedAccounts); + final Account authenticatedAccount = getAccountFromAuthentication(); + final boolean accountIsAdminRole = workspaceService.isAdminRoleUserInWorkspace(wksName, + authenticatedAccount.getUsername()); + List excludeDeleteAccounts = Collections.singletonList(authenticatedAccount); + Page userPage = new PageImpl<>(linkedAccounts, pageable, linkedAccounts.size()); var sort = userPage.getSort() .stream() .findFirst() .orElseGet(() -> asc("createdDate")); - List allAccounts = accountRepository.findAll(); - allAccounts.removeAll(accounts); - - model.addAttribute("accounts", allAccounts); + model.addAttribute("nonLinkedAccounts", nonLinkedAccounts); + model.addAttribute("isAdmin", accountIsAdminRole); + model.addAttribute("excludeDeleteAccounts", excludeDeleteAccounts); model.addAttribute("userPage", userPage); model.addAttribute("availableSizes", availableSizes); model.addAttribute("sortProp", sort.getProperty()); @@ -266,13 +262,31 @@ public String getWorkspaceUsersPage(Model model, public String addUser(@RequestParam String email, @PathVariable String wksName) { try { workspaceRoleService.addAccountToWorkspace(wksName, email); - return "redirect:/workspace/{wksName}/users/"; + return "redirect:/workspace/{wksName}/users"; } catch (WorkspaceNotFoundException e) { log.error("Workspace with name {} not found", wksName); return "redirect:/workspaces"; } catch (AccountNotFoundException e) { log.error("Account with email {} not found", email); - return "redirect:/workspace/{wksName}/users/"; + return "redirect:/workspace/{wksName}/users"; + } + } + + @DeleteMapping("/{wksName}/users") + @PreAuthorize(IS_USER_ADMIN_IN_WKS) + public String deleteUser(@RequestParam String email, @PathVariable String wksName) { + try { + workspaceRoleService.deleteAccountFromWorkspace(wksName, email); + return "redirect:/workspace/{wksName}/users"; + } catch (WorkspaceNotFoundException e) { + log.error("Workspace with name {} not found", wksName); + return "redirect:/workspaces"; + } catch (AccountNotFoundException e) { + log.error("Account with email {} not found", email); + return "redirect:/workspace/{wksName}/users"; + } catch (WorkspaceRoleNotFoundException e) { + log.error("The user with email {} has no role in the workspace {} ", email, wksName, e); + return "redirect:/workspaces"; } } @@ -287,4 +301,18 @@ private void getLastTypoDataToModel(final Model model, final String wksName) { model.addAttribute("lastTypoCreatedDate", createdDate); model.addAttribute("lastTypoCreatedDateAgo", createdDate.map(new PrettyTime()::format)); } + + private List getNonLinkedAccounts(Collection allAccounts, Collection linkedAccounts) { + final List linkedIds = linkedAccounts.stream() + .map(Account::getId) + .toList(); + return allAccounts.stream() + .filter(account -> !linkedIds.contains(account.getId())) + .collect(Collectors.toList()); + } + + private Account getAccountFromAuthentication() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return accountService.findByUsername(authentication.getName()); + } } diff --git a/src/main/java/io/hexlet/typoreporter/web/exception/AccountNotFoundException.java b/src/main/java/io/hexlet/typoreporter/web/exception/AccountNotFoundException.java index 5b07bf7b..d5c24e5e 100644 --- a/src/main/java/io/hexlet/typoreporter/web/exception/AccountNotFoundException.java +++ b/src/main/java/io/hexlet/typoreporter/web/exception/AccountNotFoundException.java @@ -8,9 +8,10 @@ public class AccountNotFoundException extends ErrorResponseException { - private static final String NOT_FOUND_MSG = "Account with email=''{0}'' not found"; + private static final String NOT_FOUND_MSG = "Account with email or userName=''{0}'' not found"; - public AccountNotFoundException(final String email) { - super(NOT_FOUND, ProblemDetail.forStatusAndDetail(NOT_FOUND, "Account not found"), null, format(NOT_FOUND_MSG, email), new Object[]{email}); + public AccountNotFoundException(final String emailOrUserName) { + super(NOT_FOUND, ProblemDetail.forStatusAndDetail(NOT_FOUND, "Account not found"), null, + format(NOT_FOUND_MSG, emailOrUserName), new Object[]{emailOrUserName}); } } diff --git a/src/main/java/io/hexlet/typoreporter/web/exception/WorkspaceRoleNotFoundException.java b/src/main/java/io/hexlet/typoreporter/web/exception/WorkspaceRoleNotFoundException.java new file mode 100644 index 00000000..f97416c6 --- /dev/null +++ b/src/main/java/io/hexlet/typoreporter/web/exception/WorkspaceRoleNotFoundException.java @@ -0,0 +1,17 @@ +package io.hexlet.typoreporter.web.exception; + +import org.springframework.http.ProblemDetail; +import org.springframework.web.ErrorResponseException; + +import static java.text.MessageFormat.format; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +public class WorkspaceRoleNotFoundException extends ErrorResponseException { + private static final String NAME_NOT_FOUND_MSG = "Workspace role with accountId=''{0}'' and workspaceId=''{1}'' not found"; + + public WorkspaceRoleNotFoundException(final Long accountId, final Long workspaceId) { + super(NOT_FOUND, ProblemDetail.forStatusAndDetail(NOT_FOUND, "Workspace role not found"), null, + format(NAME_NOT_FOUND_MSG, accountId, workspaceId), new Object[]{accountId, workspaceId}); + } + +} diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 94c21b8e..d723dd3c 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -76,6 +76,25 @@ text.script-descr=Install this script on your website: text.modified-info=Modified by {0} {1} at {2} text.created-info=Created by {0} {1} at {2} text.add-user-to-wks=Adding a user to a workspace +text.confirm-delete-user-wks=Are you sure you want to delete user from this workspace? text.hint-choose-a-user=Choose a user from the list text.wks-delete-confirm=Are you sure you want to delete this workspace? btn.add-to-wks=Add to workspace +btn.delete-from-wks=Delete from workspace + +# footer +about=About +source-code=Source Code +telegram=Telegram Hexlet (Volunteers) +help=Help +blog=Blog +knowledge-base=Knowledge Base +recommended-books=Recommended Books +other-opensource-projects=Other open-source projects +hexlet.cv=Hexlet CV +hexlet.editor=Hexlet Editor +hexlet.friends=Hexlet Friends +miscellaneous=Miscellaneous +code-basics=Code Basics +codebattle=Codebattle +hexlet.guides=Hexlet Guides diff --git a/src/main/resources/messages_ru.properties b/src/main/resources/messages_ru.properties index c8138891..ef97c02a 100644 --- a/src/main/resources/messages_ru.properties +++ b/src/main/resources/messages_ru.properties @@ -29,6 +29,7 @@ btn.close=Закрыть btn.create=Создать btn.page-size=Размер страницы btn.add-to-wks=Добавить в пространство +btn.delete-from-wks=Удалить из пространства btn.create-account=Создать Аккаунт btn.regenerate-token=Пересоздать Токен btn.delete=Удалить @@ -73,5 +74,23 @@ text.script-descr=Установите этот скрипт на ваш сай text.modified-info=Изменен пользователем {0} {1} в {2} text.created-info=Создан пользователем {0} {1} в {2} text.add-user-to-wks=Добавить пользователя в пространство +text.confirm-delete-user-wks=Вы действительно хотите удалить пользователя из рабочего пространства? text.hint-choose-a-user=Выберите пользователя из списка text.wks-delete-confirm=Удалить пространство? + +# footer +about=О проекте +source-code=Исходный Код +telegram=Telegram Hexlet (Волонтеры) +help=Help +blog=Блог +knowledge-base=База Знаний +recommended-books=Рекомендованные Книги +other-opensource-projects=Другие open-source проекты +hexlet.cv=Hexlet-резюме +hexlet.editor=Хекслет-редактор +hexlet.friends=Друзья Хекслета +miscellaneous=Дополнительно +code-basics=Code Basics +codebattle=Кодбаттл +hexlet.guides=Гайды Хекслета diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css new file mode 100644 index 00000000..a8d4cd4d --- /dev/null +++ b/src/main/resources/static/css/styles.css @@ -0,0 +1,4 @@ +.main-content { + flex: 1; + padding-top: 2rem; +} diff --git a/src/main/resources/templates/account/acc-info.html b/src/main/resources/templates/account/acc-info.html index f6c0a46b..05b08b62 100644 --- a/src/main/resources/templates/account/acc-info.html +++ b/src/main/resources/templates/account/acc-info.html @@ -1,58 +1,59 @@ - - - - -
-
-
-
-
-
-
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
+
+
+ +
-
-
- - - - - - - - - - - - - - - - - - - -
-
- +
+ + + + + + + + + + + + + + + + + + + +
+ + diff --git a/src/main/resources/templates/account/pass-update.html b/src/main/resources/templates/account/pass-update.html index 00009fd4..6a86bcab 100644 --- a/src/main/resources/templates/account/pass-update.html +++ b/src/main/resources/templates/account/pass-update.html @@ -1,41 +1,42 @@ - - - - -
-
-
-
-
- - -
-
-
- - -
-
-
- - -
-
-
-
-
- - -
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
-
-
- + + diff --git a/src/main/resources/templates/account/prof-update.html b/src/main/resources/templates/account/prof-update.html index 8d0bc6e2..bf3bd47e 100644 --- a/src/main/resources/templates/account/prof-update.html +++ b/src/main/resources/templates/account/prof-update.html @@ -1,52 +1,53 @@ - - - - -
-
-
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
- - -
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
-
-
- + + diff --git a/src/main/resources/templates/account/signup.html b/src/main/resources/templates/account/signup.html index 8bd17431..311a1513 100644 --- a/src/main/resources/templates/account/signup.html +++ b/src/main/resources/templates/account/signup.html @@ -1,89 +1,88 @@ - - - - -
-
-
-
-
- - -
-

+ + +
+
+
+ +
+ + +
+

+
-
-
- - -
-

+
+ + +
+

+
-
-
- - -
-

+
+ + +
+

+
-
-
- - -
-

+
+ + +
+

+
-
-
- - -
-

+
+ + +
+

+
-
-
- - -
-

+
+ + +
+

+
-
-
- - -
-

+
+ + +
+

+
-
- - + + +
-
-
+
- diff --git a/src/main/resources/templates/application.html b/src/main/resources/templates/application.html new file mode 100644 index 00000000..ecec40e8 --- /dev/null +++ b/src/main/resources/templates/application.html @@ -0,0 +1,15 @@ + + + + +
+ + +
+ +
+
+ + diff --git a/src/main/resources/templates/create-workspace.html b/src/main/resources/templates/create-workspace.html index 88c0a31f..11d8e06f 100644 --- a/src/main/resources/templates/create-workspace.html +++ b/src/main/resources/templates/create-workspace.html @@ -1,39 +1,40 @@ - - - - -
-
-
-
-
- - -
-
-
- - -
-
-
- - -
-
- -
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
-
-
- + + diff --git a/src/main/resources/templates/error-general.html b/src/main/resources/templates/error-general.html index 9fbfdc1c..54df2eb0 100644 --- a/src/main/resources/templates/error-general.html +++ b/src/main/resources/templates/error-general.html @@ -2,8 +2,10 @@ -
- -
+
+
+ +
+
diff --git a/src/main/resources/templates/fragments/footer.html b/src/main/resources/templates/fragments/footer.html new file mode 100644 index 00000000..2fd9e661 --- /dev/null +++ b/src/main/resources/templates/fragments/footer.html @@ -0,0 +1,92 @@ + + + +
+
+
+
+ +

© Hexlet

+
+ +
+
+

+ +
+
+

+ +
+
+

+ +
+
+
+
+ + diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 2d4c2f1c..43851dbb 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -13,6 +13,7 @@ + diff --git a/src/main/resources/templates/fragments/panels.html b/src/main/resources/templates/fragments/panels.html index b5085e4b..4114c02d 100644 --- a/src/main/resources/templates/fragments/panels.html +++ b/src/main/resources/templates/fragments/panels.html @@ -3,82 +3,82 @@ xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> - + +
    +
  • +
+
+
+ + + + + Integration
- - - -
    -
  • -
-
-
- - - - - Integration -
-
+
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 9e4b8376..61ce3206 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -6,7 +6,7 @@ -
+

Hexlet Typo Reporter

Сервис для отправки сообщений об ошибках в тексте на Вашем сайте

@@ -27,98 +27,8 @@

Hexlet Typo Reporter

+
- diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index c99d477e..562222fd 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -1,34 +1,34 @@ - - - - -
-
-
- - -
-
- - + + +
+
+
+ + + +
+ + +
+
+ + +
+ +
-
- - +
+
+
+
- - -
-
-
-
- -
-
-
+
+
- diff --git a/src/main/resources/templates/workspace/wks-info.html b/src/main/resources/templates/workspace/wks-info.html index 6077b57b..73a71afb 100644 --- a/src/main/resources/templates/workspace/wks-info.html +++ b/src/main/resources/templates/workspace/wks-info.html @@ -1,61 +1,62 @@ - - - - -
-
-
-
-
-
-

-
-
-
-
-
-
-
-

-

-
- - -
-
- -
- + + diff --git a/src/main/resources/templates/workspace/wks-settings.html b/src/main/resources/templates/workspace/wks-settings.html index 16bbeec2..fd2b037e 100644 --- a/src/main/resources/templates/workspace/wks-settings.html +++ b/src/main/resources/templates/workspace/wks-settings.html @@ -1,47 +1,52 @@ - - - - -
-
-
-
-
-
-

-
-
-
-
-
- -
+ + +
+
+
+
+
+
+

-
-
-
-

:

-

: Authorization: Basic [[${wksBasicToken}]]

+
+
+
+
+ +
+
-
-
-
-

-

+                
+
+

:

+

: Authorization: Basic [[${wksBasicToken}]]

+
+
+
+
+

+
+                        
+<script src="https://cdn.jsdelivr.net/gh/hexlet/hexlet-correction@main/src/widget/index.js"></script>
+
 <script>
-    handleTypoReporter({ authorizationToken: [[${wksBasicToken}]],
-    workSpaceUrl: [[${rootUrl}]], workSpaceId: [[${wksId}]]})
+    handleTypoReporter({ authorizationToken: '[[${wksBasicToken}]]',
+    workSpaceUrl: '[[${rootUrl}]]', workSpaceId: '[[${wksId}]]'})
 </script>
-    
+
+
+
-
-
- + + diff --git a/src/main/resources/templates/workspace/wks-typos.html b/src/main/resources/templates/workspace/wks-typos.html index 91bf0711..c7be9e24 100644 --- a/src/main/resources/templates/workspace/wks-typos.html +++ b/src/main/resources/templates/workspace/wks-typos.html @@ -1,172 +1,175 @@ - - - - -
-
- -
-
- -
- -
-

-
- -
- -
- -
+ + diff --git a/src/main/resources/templates/workspace/wks-update.html b/src/main/resources/templates/workspace/wks-update.html index b7bf1f6c..b1ca35f8 100644 --- a/src/main/resources/templates/workspace/wks-update.html +++ b/src/main/resources/templates/workspace/wks-update.html @@ -1,63 +1,64 @@ - - - - -
-
-
-
-
-
-

-
-
-
-
-
- -
- - -
-
-
- - -
-
- - -
+ + +
+
+
+
+
+
+

-
-
    -
  • - [[*{key.toString()}]][[*{value}]] -
  • -
  • - TOTAL -
  • -
+
+
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+
    +
  • + [[*{key.toString()}]][[*{value}]] +
  • +
  • + TOTAL +
  • +
+
-
-
- +
+ diff --git a/src/main/resources/templates/workspace/wks-users.html b/src/main/resources/templates/workspace/wks-users.html index a606c50a..773fb1e7 100644 --- a/src/main/resources/templates/workspace/wks-users.html +++ b/src/main/resources/templates/workspace/wks-users.html @@ -1,139 +1,155 @@ - - - - -
-
- -
-
- -
- + + diff --git a/src/main/resources/templates/workspaces.html b/src/main/resources/templates/workspaces.html index 944d1cea..c0a11e8f 100644 --- a/src/main/resources/templates/workspaces.html +++ b/src/main/resources/templates/workspaces.html @@ -1,22 +1,24 @@ - - - - -
-
-
-
-
-
-

- - - + + +
+
+
+
+
+
+

+
+
-
-
- + + diff --git a/src/widget/index.html b/src/widget/index.html index f33c4554..c2757d69 100644 --- a/src/widget/index.html +++ b/src/widget/index.html @@ -216,10 +216,11 @@

Guides

Created by the Bootstrap team · © 2023 - - + + + handleTypoReporter({ authorizationToken: 'MjQyOmVlYTZkMWE1LTNkMGQtNDg1Yi04OGMwLWVkOGU1YTRlOGZjMA==', + workSpaceUrl: 'https://hexlet-correction.herokuapp.com', workSpaceId: '242'}) + diff --git a/src/widget/index.js b/src/widget/index.js index 48b3e097..78d90fe1 100644 --- a/src/widget/index.js +++ b/src/widget/index.js @@ -221,7 +221,7 @@ const handleTypoReporter = (options) => { data.reporterName = name.value === '' ? 'Anonymous' : name.value; data.reporterComment = commentField.value; try { - await fetch(`${workSpaceUrl}/${workSpaceId}/typos`, { + await fetch(`${workSpaceUrl}/api/workspaces/${workSpaceId}/typos`, { method: 'POST', headers: { 'Content-Type': 'application/json',