diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/AccountController.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/AccountController.java new file mode 100644 index 00000000..8b066e2e --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/AccountController.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.application.controllers.account; + +import com.svalyn.studio.application.controllers.account.dto.AccountDTO; +import com.svalyn.studio.application.controllers.account.dto.CreateAccountInput; +import com.svalyn.studio.application.controllers.dto.IPayload; +import com.svalyn.studio.application.controllers.dto.PageInfoWithCount; +import com.svalyn.studio.application.services.account.api.IAccountService; +import graphql.relay.Connection; +import graphql.relay.DefaultConnection; +import graphql.relay.DefaultConnectionCursor; +import graphql.relay.DefaultEdge; +import graphql.relay.Edge; +import graphql.relay.Relay; +import jakarta.validation.Valid; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; + +import java.util.Objects; + +/** + * Controller used to retrieve accounts. + * + * @author sbegaudeau + */ +@Controller +public class AccountController { + + private final IAccountService accountService; + + public AccountController(IAccountService accountService) { + this.accountService = Objects.requireNonNull(accountService); + } + + @SchemaMapping(typeName = "Admin") + public Connection accounts(@Argument int page, @Argument int rowsPerPage) { + var pageData = this.accountService.findAll(page, rowsPerPage); + var edges = pageData.stream().map(account -> { + var value = new Relay().toGlobalId("Account", account.username()); + var cursor = new DefaultConnectionCursor(value); + return (Edge) new DefaultEdge<>(account, cursor); + }).toList(); + var pageInfo = new PageInfoWithCount(null, null, pageData.hasPrevious(), pageData.hasNext(), pageData.getTotalElements()); + return new DefaultConnection<>(edges, pageInfo); + } + + @MutationMapping + public IPayload createAccount(@Argument @Valid CreateAccountInput input) { + return this.accountService.createAccount(input); + } +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/AccountDTO.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/AccountDTO.java new file mode 100644 index 00000000..1ba368de --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/AccountDTO.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.application.controllers.account.dto; + +import com.svalyn.studio.domain.account.AccountRole; +import jakarta.validation.constraints.NotNull; + +import java.time.Instant; + +public record AccountDTO( + @NotNull String name, + @NotNull String username, + @NotNull String imageUrl, + @NotNull String email, + @NotNull AccountRole role, + @NotNull Instant createdOn, + @NotNull Instant lastModifiedOn + ) { +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/CreateAccountInput.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/CreateAccountInput.java new file mode 100644 index 00000000..54393142 --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/CreateAccountInput.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.application.controllers.account.dto; + +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +/** + * Input used to create new account. + * + * @author sbegaudeau + */ +public record CreateAccountInput( + @NotNull UUID id, + @NotNull String name, + @NotNull String username, + @NotNull String email, + @NotNull String password +) { +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/CreateAccountSuccessPayload.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/CreateAccountSuccessPayload.java new file mode 100644 index 00000000..6753cd59 --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/dto/CreateAccountSuccessPayload.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.application.controllers.account.dto; + +import com.svalyn.studio.application.controllers.dto.IPayload; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +/** + * Payload used to indicate that the account has been created. + * + * @author sbegaudeau + */ +public record CreateAccountSuccessPayload(@NotNull UUID id, @NotNull AccountDTO account) implements IPayload { +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/admin/AdminController.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/admin/AdminController.java new file mode 100644 index 00000000..448ff398 --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/admin/AdminController.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.application.controllers.admin; + +import com.svalyn.studio.application.controllers.admin.dto.AdminDTO; +import com.svalyn.studio.application.controllers.viewer.Viewer; +import com.svalyn.studio.domain.account.AccountRole; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; + +/** + * Controller used to retrieve the admin. + * + * @author sbegaudeau + */ +@Controller +public class AdminController { + @SchemaMapping(typeName = "Viewer") + public AdminDTO asAdmin(Viewer viewer) { + if (viewer.role().equals(AccountRole.ADMIN)) { + return new AdminDTO(); + } + return null; + } +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/admin/dto/AdminDTO.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/admin/dto/AdminDTO.java new file mode 100644 index 00000000..444c1672 --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/admin/dto/AdminDTO.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.application.controllers.admin.dto; + +/** + * The admin DTO for the GraphQL layer. + * + * @author sbegaudeau + */ +public record AdminDTO() { +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/viewer/Viewer.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/viewer/Viewer.java index 54eaabd9..5109751f 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/viewer/Viewer.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/viewer/Viewer.java @@ -18,6 +18,7 @@ */ package com.svalyn.studio.application.controllers.viewer; +import com.svalyn.studio.domain.account.AccountRole; import jakarta.validation.constraints.NotNull; /** @@ -25,5 +26,9 @@ * * @author sbegaudeau */ -public record Viewer(@NotNull String name, @NotNull String username, @NotNull String imageUrl) { +public record Viewer( + @NotNull String name, + @NotNull String username, + @NotNull String imageUrl, + @NotNull AccountRole role) { } diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java index 2f0d93d6..57c3ff90 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java @@ -19,11 +19,23 @@ package com.svalyn.studio.application.services.account; +import com.svalyn.studio.application.controllers.account.dto.AccountDTO; +import com.svalyn.studio.application.controllers.account.dto.CreateAccountInput; +import com.svalyn.studio.application.controllers.account.dto.CreateAccountSuccessPayload; +import com.svalyn.studio.application.controllers.dto.ErrorPayload; +import com.svalyn.studio.application.controllers.dto.IPayload; import com.svalyn.studio.application.controllers.dto.ProfileDTO; import com.svalyn.studio.application.controllers.viewer.Viewer; import com.svalyn.studio.application.services.account.api.IAccountService; import com.svalyn.studio.application.services.account.api.IAvatarUrlService; +import com.svalyn.studio.domain.Failure; +import com.svalyn.studio.domain.Success; +import com.svalyn.studio.domain.account.Account; import com.svalyn.studio.domain.account.repositories.IAccountRepository; +import com.svalyn.studio.domain.account.services.api.IAccountCreationService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,17 +53,33 @@ public class AccountService implements IAccountService { private final IAccountRepository accountRepository; + private final IAccountCreationService accountCreationService; + private final IAvatarUrlService avatarUrlService; - public AccountService(IAccountRepository accountRepository, IAvatarUrlService avatarUrlService) { + public AccountService(IAccountRepository accountRepository, IAccountCreationService accountCreationService, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); + this.accountCreationService = accountCreationService; this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } + private AccountDTO toDTO(Account account) { + return new AccountDTO( + account.getName(), + account.getUsername(), + this.avatarUrlService.imageUrl(account.getUsername()), + account.getEmail(), + account.getRole(), + account.getCreatedOn(), + account.getLastModifiedOn() + ); + } + @Override @Transactional(readOnly = true) public Optional findViewerById(UUID id) { - return this.accountRepository.findById(id).map(account -> new Viewer(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); + return this.accountRepository.findById(id) + .map(account -> new Viewer(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()), account.getRole())); } @Override @@ -59,4 +87,29 @@ public Optional findViewerById(UUID id) { public Optional findProfileByUsername(String username) { return this.accountRepository.findByUsername(username).map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()), account.getCreatedOn())); } + + @Override + @Transactional(readOnly = true) + public Page findAll(int page, int rowsPerPage) { + var accounts = this.accountRepository.findAll(page * rowsPerPage, rowsPerPage).stream() + .map(this::toDTO) + .toList(); + var count = this.accountRepository.count(); + return new PageImpl<>(accounts, PageRequest.of(page, rowsPerPage), count); + } + + @Override + @Transactional + public IPayload createAccount(CreateAccountInput input) { + IPayload payload = null; + + var result = this.accountCreationService.createAccount(input.name(), input.email(), input.username(), input.password()); + if (result instanceof Failure failure) { + payload = new ErrorPayload(input.id(), failure.message()); + } else if (result instanceof Success success) { + payload = new CreateAccountSuccessPayload(input.id(), this.toDTO(success.data())); + } + + return payload; + } } diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAccountService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAccountService.java index 55df928c..749e426b 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAccountService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAccountService.java @@ -19,8 +19,12 @@ package com.svalyn.studio.application.services.account.api; +import com.svalyn.studio.application.controllers.account.dto.AccountDTO; +import com.svalyn.studio.application.controllers.account.dto.CreateAccountInput; +import com.svalyn.studio.application.controllers.dto.IPayload; import com.svalyn.studio.application.controllers.dto.ProfileDTO; import com.svalyn.studio.application.controllers.viewer.Viewer; +import org.springframework.data.domain.Page; import java.util.Optional; import java.util.UUID; @@ -35,4 +39,8 @@ public interface IAccountService { Optional findViewerById(UUID id); Optional findProfileByUsername(String username); + + Page findAll(int page, int rowsPerPage); + + IPayload createAccount(CreateAccountInput input); } diff --git a/backend/svalyn-studio-application/src/main/resources/graphql/svalyn-studio.graphqls b/backend/svalyn-studio-application/src/main/resources/graphql/svalyn-studio.graphqls index fbf6cbb2..1a57526f 100644 --- a/backend/svalyn-studio-application/src/main/resources/graphql/svalyn-studio.graphqls +++ b/backend/svalyn-studio-application/src/main/resources/graphql/svalyn-studio.graphqls @@ -16,6 +16,8 @@ type Viewer { name: String! username: String! imageUrl: String! + role: Role! + asAdmin: Admin invitations(page: Int!, rowsPerPage: Int!): ViewerInvitationsConnection! profile(username: String!): Profile organizations: ViewerOrganizationsConnection! @@ -32,6 +34,34 @@ type Viewer { search(query: String!): SearchResults! } +enum Role { + USER + ADMIN +} + +type Admin { + accounts(page: Int!, rowsPerPage: Int!): AdminAccountsConnection! +} + +type AdminAccountsConnection { + edges: [AdminAccountsEdge]! + pageInfo: PageInfo! +} + +type AdminAccountsEdge { + node: Account! +} + +type Account { + name: String! + username: String! + imageUrl: String! + email: String! + role: Role! + createdOn: Instant! + lastModifiedOn: Instant! +} + type ViewerInvitationsConnection { edges: [ViewerInvitationsEdge!]! pageInfo: PageInfo! @@ -451,6 +481,7 @@ type EnumerationLiteral { } type Mutation { + createAccount(input: CreateAccountInput!): CreateAccountPayload! @validated createAuthenticationToken(input: CreateAuthenticationTokenInput!): CreateAuthenticationTokenPayload! @validated updateAuthenticationTokensStatus(input: UpdateAuthenticationTokensStatusInput!): UpdateAuthenticationTokensStatusPayload! @validated createOrganization(input: CreateOrganizationInput!): CreateOrganizationPayload! @validated @@ -488,6 +519,21 @@ type SuccessPayload { id: ID! } +input CreateAccountInput { + id: ID! + name: String! + email: String! + username: String! + password: String! +} + +union CreateAccountPayload = ErrorPayload | CreateAccountSuccessPayload + +type CreateAccountSuccessPayload { + id: ID! + account: Account! +} + input CreateAuthenticationTokenInput { id: ID! name: String! diff --git a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java index f6afd1e7..bae524a3 100644 --- a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java +++ b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java @@ -24,6 +24,7 @@ import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -34,6 +35,8 @@ */ @Repository public interface IAccountRepository extends PagingAndSortingRepository, ListCrudRepository { + boolean existsByUsername(String username); + Optional findByUsername(String username); Optional findByEmail(String email); @@ -51,4 +54,12 @@ public interface IAccountRepository extends PagingAndSortingRepository findByAccessKey(String accessKey); + + @Query(""" + SELECT * FROM account account + ORDER BY account.username ASC + LIMIT :limit + OFFSET :offset + """) + List findAll(long offset, int limit); } diff --git a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/services/AccountCreationService.java b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/services/AccountCreationService.java new file mode 100644 index 00000000..9ccedbe8 --- /dev/null +++ b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/services/AccountCreationService.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.domain.account.services; + +import com.svalyn.studio.domain.Failure; +import com.svalyn.studio.domain.IResult; +import com.svalyn.studio.domain.Success; +import com.svalyn.studio.domain.account.Account; +import com.svalyn.studio.domain.account.AccountRole; +import com.svalyn.studio.domain.account.PasswordCredentials; +import com.svalyn.studio.domain.account.repositories.IAccountRepository; +import com.svalyn.studio.domain.account.services.api.IAccountCreationService; +import com.svalyn.studio.domain.message.api.IMessageService; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Used to create accounts. + * + * @author sbegaudeau + */ +@Service +public class AccountCreationService implements IAccountCreationService { + + private final IAccountRepository accountRepository; + + private final PasswordEncoder passwordEncoder; + + private final IMessageService messageService; + + public AccountCreationService(IAccountRepository accountRepository, PasswordEncoder passwordEncoder, IMessageService messageService) { + this.accountRepository = Objects.requireNonNull(accountRepository); + this.passwordEncoder = Objects.requireNonNull(passwordEncoder); + this.messageService = Objects.requireNonNull(messageService); + } + + @Override + public IResult createAccount(String name, String email, String username, String password) { + IResult result = null; + + var isAdmin = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(authentication -> authentication.getAuthorities().stream() + .filter(SimpleGrantedAuthority.class::isInstance) + .map(SimpleGrantedAuthority.class::cast) + .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_" + AccountRole.ADMIN))) + .orElse(Boolean.FALSE) + .booleanValue(); + + var alreadyExist = this.accountRepository.existsByUsername(username); + if (!isAdmin) { + result = new Failure<>(this.messageService.unauthorized()); + } else if (alreadyExist) { + result = new Failure<>(this.messageService.alreadyExists("account")); + } else { + var passwordCredentials = PasswordCredentials.newPasswordCredentials() + .password(this.passwordEncoder.encode(password)) + .build(); + + var account = Account.newAccount() + .name(name) + .username(username) + .email(email) + .passwordCredentials(Set.of(passwordCredentials)) + .oAuth2Metadata(Set.of()) + .role(AccountRole.USER) + .build(); + + this.accountRepository.save(account); + result = new Success<>(account); + } + + return result; + } +} diff --git a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/services/api/IAccountCreationService.java b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/services/api/IAccountCreationService.java new file mode 100644 index 00000000..f32c2181 --- /dev/null +++ b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/services/api/IAccountCreationService.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.domain.account.services.api; + +import com.svalyn.studio.domain.IResult; +import com.svalyn.studio.domain.account.Account; + +/** + * Used to create accounts. + * + * @author sbegaudeau + */ +public interface IAccountCreationService { + IResult createAccount(String name, String email, String username, String password); +} diff --git a/frontend/svalyn-studio-app/src/admin/AdminAccountsView.tsx b/frontend/svalyn-studio-app/src/admin/AdminAccountsView.tsx new file mode 100644 index 00000000..b35397c0 --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminAccountsView.tsx @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { gql, useQuery } from '@apollo/client'; +import PersonIcon from '@mui/icons-material/Person'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Container from '@mui/material/Container'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; +import { useSnackbar } from 'notistack'; +import { useEffect, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { AccountRowProps, AccountTableState, GetAccountsData, GetAccountsVariables } from './AdminAccountsView.types'; + +const getAccountsQuery = gql` + query getAccounts($page: Int!, $rowsPerPage: Int!) { + viewer { + asAdmin { + accounts(page: $page, rowsPerPage: $rowsPerPage) { + edges { + node { + username + name + email + imageUrl + role + createdOn + lastModifiedOn + } + } + pageInfo { + count + } + } + } + } + } +`; + +export const AdminAccountsView = () => { + return ( + theme.spacing(4) }}> + + theme.spacing(2) }}> + Accounts + + theme.spacing(2) }}> + theme.spacing(2), + }} + > + + + + + + + + ); +}; + +const AccountTable = () => { + const [state, setState] = useState({ + page: 0, + rowsPerPage: 20, + }); + + const variables: GetAccountsVariables = { + page: state.page, + rowsPerPage: state.rowsPerPage, + }; + const { data, error } = useQuery(getAccountsQuery, { variables }); + + const { enqueueSnackbar } = useSnackbar(); + + useEffect(() => { + if (error) { + enqueueSnackbar(error.message, { variant: 'error' }); + } + }, [error]); + + const handlePageChange = (_: React.MouseEvent | null, page: number) => { + setState((prevState) => ({ ...prevState, page })); + }; + + if (!data) { + return null; + } + return ( + <> + + + + + Avatar + Username + Name + Email + Role + Created On + Last Modified On + + + + + {data.viewer.asAdmin.accounts.edges + .map((edge) => edge.node) + .map((account) => ( + + ))} + +
+
+ + + ); +}; + +const AccountRow = ({ account }: AccountRowProps) => { + return ( + + + + + {account.username} + {account.name} + {account.email} + {account.role.toLowerCase()} + 2023/03/02 + 2023/05/17 + + ); +}; diff --git a/frontend/svalyn-studio-app/src/admin/AdminAccountsView.types.ts b/frontend/svalyn-studio-app/src/admin/AdminAccountsView.types.ts new file mode 100644 index 00000000..e85446e2 --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminAccountsView.types.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export interface AccountTableState { + page: number; + rowsPerPage: number; +} + +export interface GetAccountsData { + viewer: Viewer; +} + +export interface Viewer { + asAdmin: Admin; +} + +export interface Admin { + accounts: AdminAccountsConnection; +} + +export interface AdminAccountsConnection { + edges: AdminAccountsEdge[]; + pageInfo: PageInfo; +} + +export interface PageInfo { + count: number; +} + +export interface AdminAccountsEdge { + node: Account; +} + +export interface Account { + username: string; + name: string; + email: string; + imageUrl: string; + role: AccountRole; + createdOn: string; + lastModifiedOn: string; +} + +export type AccountRole = 'USER' | 'ADMIN'; + +export interface GetAccountsVariables { + page: number; + rowsPerPage: number; +} + +export interface AccountRowProps { + account: Account; +} diff --git a/frontend/svalyn-studio-app/src/admin/AdminHomeView.tsx b/frontend/svalyn-studio-app/src/admin/AdminHomeView.tsx new file mode 100644 index 00000000..a9b97316 --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminHomeView.tsx @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import { Link as RouterLink } from 'react-router-dom'; + +export const AdminHomeView = () => { + return ( + theme.spacing(4) }}> + + theme.spacing(40) }}> + + + Accounts + + + Have a look at all the user accounts, create new ones and edit their properties + + + + + + + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/admin/AdminNewAccountView.tsx b/frontend/svalyn-studio-app/src/admin/AdminNewAccountView.tsx new file mode 100644 index 00000000..40fec869 --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminNewAccountView.tsx @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import PersonIcon from '@mui/icons-material/Person'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Container from '@mui/material/Container'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { useSnackbar } from 'notistack'; +import { useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; +import { hasMinLength, useForm } from '../forms/useForm'; +import { AdminNewAccountViewFormData } from './AdminNewAccountView.types'; +import { useCreateAccount } from './useCreateAccount'; +import { CreateAccountInput } from './useCreateAccount.types'; + +export const AdminNewAccountView = () => { + const { data, isFormValid, getTextFieldProps } = useForm({ + initialValue: { + name: '', + email: '', + username: '', + password: '', + }, + validationRules: { + name: (data) => hasMinLength(data.name, 1), + email: (data) => hasMinLength(data.email, 1), + username: (data) => hasMinLength(data.username, 1), + password: (data) => hasMinLength(data.password, 8), + }, + }); + + const { enqueueSnackbar } = useSnackbar(); + + const [createAccount, { account, message }] = useCreateAccount(); + useEffect(() => { + if (message) { + enqueueSnackbar(message.body, { variant: message.severity }); + } + }, [account, message]); + + const handleCreateAccount: React.FormEventHandler = (event) => { + event.preventDefault(); + + const input: CreateAccountInput = { + id: crypto.randomUUID(), + name: data.name, + email: data.email, + password: data.password, + username: data.username, + }; + createAccount(input); + }; + + if (account) { + return ; + } + + return ( + theme.spacing(4) }}> + + + theme.spacing(2) }}> +
+ + + Let's create an account + + + + + + + +
+
+
+
+ ); +}; diff --git a/frontend/svalyn-studio-app/src/admin/AdminNewAccountView.types.ts b/frontend/svalyn-studio-app/src/admin/AdminNewAccountView.types.ts new file mode 100644 index 00000000..3ba86b4c --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminNewAccountView.types.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export interface AdminNewAccountViewFormData { + name: string; + email: string; + username: string; + password: string; +} diff --git a/frontend/svalyn-studio-app/src/admin/AdminRouter.tsx b/frontend/svalyn-studio-app/src/admin/AdminRouter.tsx new file mode 100644 index 00000000..d5c0d2ff --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminRouter.tsx @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { Route, Routes } from 'react-router-dom'; +import { AdminAccountsView } from './AdminAccountsView'; +import { AdminHomeView } from './AdminHomeView'; +import { AdminNewAccountView } from './AdminNewAccountView'; +import { AdminShell } from './AdminShell'; +import { ProtectedRoute } from './ProtectedRoute'; + +export const AdminRouter = () => { + return ( + + + + } /> + } /> + } /> + + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/admin/AdminShell.tsx b/frontend/svalyn-studio-app/src/admin/AdminShell.tsx new file mode 100644 index 00000000..57e61d7f --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminShell.tsx @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; +import HomeIcon from '@mui/icons-material/Home'; +import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; +import Box from '@mui/material/Box'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import { Link as RouterLink } from 'react-router-dom'; +import { useRouteMatch } from '../hooks/useRouteMatch'; +import { Navbar } from '../navbars/Navbar'; +import { AdminShellProps } from './AdminShell.types'; + +const patterns = ['/admin', '/admin/accounts']; + +export const AdminShell = ({ children }: AdminShellProps) => { + const routeMatch = useRouteMatch(patterns); + const currentTab = routeMatch?.pattern.path; + + return ( + + + + theme.spacing(0.5) }} + > + Admin + + + + + `1px solid ${theme.palette.divider}`, minWidth: (theme) => theme.spacing(30) }} + > + + + + theme.spacing(1), justifyContent: 'center' }}> + + + + + + + + theme.spacing(1), justifyContent: 'center' }}> + + + + + + + + + {children} + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/admin/AdminShell.types.ts b/frontend/svalyn-studio-app/src/admin/AdminShell.types.ts new file mode 100644 index 00000000..f2901dad --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/AdminShell.types.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export interface AdminShellProps { + children: React.ReactNode; +} diff --git a/frontend/svalyn-studio-app/src/admin/ProtectedRoute.tsx b/frontend/svalyn-studio-app/src/admin/ProtectedRoute.tsx new file mode 100644 index 00000000..ad860beb --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/ProtectedRoute.tsx @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { gql, useQuery } from '@apollo/client'; +import { useSnackbar } from 'notistack'; +import { useEffect } from 'react'; +import { NotFoundView } from '../notfound/NotFoundView'; +import { GetViewerData, GetViewerVariables, ProtectedRouteProps } from './ProtectedRoute.types'; + +const getViewerQuery = gql` + query getViewer { + viewer { + name + username + imageUrl + role + } + } +`; + +export const ProtectedRoute = ({ children, expectedRole }: ProtectedRouteProps) => { + const { enqueueSnackbar } = useSnackbar(); + + const { data, error } = useQuery(getViewerQuery); + useEffect(() => { + if (error) { + enqueueSnackbar(error.message, { variant: 'error' }); + } + }, [error]); + + if (data) { + if (data.viewer) { + const isAllowed = data.viewer.role === 'ADMIN' || (data.viewer.role === 'USER' && expectedRole === 'USER'); + if (isAllowed) { + return
{children}
; + } else { + return ; + } + } + } + return null; +}; diff --git a/frontend/svalyn-studio-app/src/admin/ProtectedRoute.types.ts b/frontend/svalyn-studio-app/src/admin/ProtectedRoute.types.ts new file mode 100644 index 00000000..7c4c42cd --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/ProtectedRoute.types.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export interface ProtectedRouteProps { + children: React.ReactNode; + expectedRole: AccountRole; +} + +export interface GetViewerData { + viewer: Viewer; +} + +export interface Viewer { + role: AccountRole; +} + +export type AccountRole = 'USER' | 'ADMIN'; + +export interface GetViewerVariables {} diff --git a/frontend/svalyn-studio-app/src/admin/useCreateAccount.tsx b/frontend/svalyn-studio-app/src/admin/useCreateAccount.tsx new file mode 100644 index 00000000..53b630b1 --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/useCreateAccount.tsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { gql, useMutation } from '@apollo/client'; +import { Message } from '../snackbar/Message.types'; +import { + Account, + CreateAccountData, + CreateAccountInput, + CreateAccountResult, + CreateAccountSuccessPayload, + CreateAccountVariables, + ErrorPayload, + UseCreateAccountValue, +} from './useCreateAccount.types'; + +const createAccountMutation = gql` + mutation createAccount($input: CreateAccountInput!) { + createAccount(input: $input) { + __typename + ... on CreateAccountSuccessPayload { + account { + username + } + } + ... on ErrorPayload { + message + } + } + } +`; + +export const useCreateAccount = (): UseCreateAccountValue => { + const [performAccountCreation, { loading, data, error }] = useMutation( + createAccountMutation + ); + + const createAccount = (input: CreateAccountInput) => { + performAccountCreation({ variables: { input } }); + }; + + let accountCreationData: Account | null = null; + let accountCreationMessage: Message | null = null; + + if (data) { + const { createAccount } = data; + if (createAccount.__typename === 'CreateAccountSuccessPayload') { + const createAccountSuccessPayload = createAccount as CreateAccountSuccessPayload; + accountCreationData = createAccountSuccessPayload.account; + } else if (createAccount.__typename === 'ErrorPayload') { + const errorPayload = createAccount as ErrorPayload; + accountCreationMessage = { body: errorPayload.message, severity: 'error' }; + } + } + if (error) { + accountCreationMessage = { body: error.message, severity: 'error' }; + } + + const result: CreateAccountResult = { + loading, + account: accountCreationData, + message: accountCreationMessage, + }; + + return [createAccount, result]; +}; diff --git a/frontend/svalyn-studio-app/src/admin/useCreateAccount.types.ts b/frontend/svalyn-studio-app/src/admin/useCreateAccount.types.ts new file mode 100644 index 00000000..60cacace --- /dev/null +++ b/frontend/svalyn-studio-app/src/admin/useCreateAccount.types.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { Message } from '../snackbar/Message.types'; + +export type UseCreateAccountValue = [createAccount: (input: CreateAccountInput) => void, result: CreateAccountResult]; + +export interface CreateAccountInput { + id: string; + name: string; + username: string; + password: string; + email: string; +} + +export interface CreateAccountPayload { + __typename: string; +} + +export interface ErrorPayload extends CreateAccountPayload { + __typename: 'ErrorPayload'; + message: string; +} + +export interface CreateAccountSuccessPayload extends CreateAccountPayload { + __typename: 'CreateAccountSuccessPayload'; + account: Account; +} + +export interface CreateAccountVariables { + input: CreateAccountInput; +} + +export interface CreateAccountData { + createAccount: CreateAccountPayload; +} + +export interface CreateAccountResult { + loading: boolean; + account: Account | null; + message: Message | null; +} + +export interface Account { + username: string; +} diff --git a/frontend/svalyn-studio-app/src/app/App.tsx b/frontend/svalyn-studio-app/src/app/App.tsx index a42a6a39..b422ca36 100644 --- a/frontend/svalyn-studio-app/src/app/App.tsx +++ b/frontend/svalyn-studio-app/src/app/App.tsx @@ -18,6 +18,7 @@ */ import { Route, Routes } from 'react-router-dom'; +import { AdminRouter } from '../admin/AdminRouter'; import { ChangeProposalsRouter } from '../changeproposals/ChangeProposalsRouter'; import { DomainsRouter } from '../domains/DomainsRouter'; import { ErrorsRouter } from '../errors/ErrorsRouter'; @@ -53,6 +54,7 @@ export const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/svalyn-studio-app/src/navbars/Navbar.tsx b/frontend/svalyn-studio-app/src/navbars/Navbar.tsx index 714862f9..20721a5b 100644 --- a/frontend/svalyn-studio-app/src/navbars/Navbar.tsx +++ b/frontend/svalyn-studio-app/src/navbars/Navbar.tsx @@ -43,6 +43,7 @@ const getViewerQuery = gql` name username imageUrl + role unreadNotificationsCount } } @@ -50,24 +51,16 @@ const getViewerQuery = gql` export const Navbar = ({ children }: NavbarProps) => { const [state, setState] = useState({ - viewer: null, anchorElement: null, }); const { enqueueSnackbar } = useSnackbar(); - - const { loading, data, error } = useQuery(getViewerQuery); + const { data, error } = useQuery(getViewerQuery); useEffect(() => { - if (!loading) { - if (data) { - const { viewer } = data; - setState((prevState) => ({ ...prevState, viewer })); - } - if (error) { - enqueueSnackbar(error.message, { variant: 'error' }); - } + if (error) { + enqueueSnackbar(error.message, { variant: 'error' }); } - }, [loading, data, error]); + }, [error]); const { openPalette }: PaletteContextValue = useContext(PaletteContext); @@ -86,7 +79,7 @@ export const Navbar = ({ children }: NavbarProps) => { {children} - {state.viewer !== null ? ( + {data?.viewer ? ( <> { - + - + { +export const UserMenu = ({ viewer, onClose, ...props }: UserMenuProps) => { const [state, setState] = useState({ redirectToLogin: false, }); @@ -68,7 +69,7 @@ export const UserMenu = ({ name, username, onClose, ...props }: UserMenuProps) = return ( - + @@ -76,7 +77,7 @@ export const UserMenu = ({ name, username, onClose, ...props }: UserMenuProps) = Dashboard - + @@ -94,6 +95,14 @@ export const UserMenu = ({ name, username, onClose, ...props }: UserMenuProps) = Settings + {viewer.role === 'ADMIN' ? ( + + + + + Admin Panel + + ) : null} diff --git a/frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts b/frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts index f45ade6d..ee7c1d3d 100644 --- a/frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts +++ b/frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts @@ -20,10 +20,17 @@ import { MenuProps } from '@mui/material/Menu'; export interface UserMenuProps extends MenuProps { + viewer: Viewer; +} + +export interface Viewer { name: string; username: string; + role: AccountRole; } +export type AccountRole = 'USER' | 'ADMIN'; + export interface UserMenuState { redirectToLogin: boolean; }