Skip to content

Commit

Permalink
[230] Add the ability to create accounts for the server administrator
Browse files Browse the repository at this point in the history
Bug: #230
Signed-off-by: Stéphane Bégaudeau <[email protected]>
  • Loading branch information
sbegaudeau committed Aug 26, 2023
1 parent 4178c7f commit 77c90d8
Show file tree
Hide file tree
Showing 30 changed files with 1,376 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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<AccountDTO> 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<AccountDTO>) 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);
}
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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() {
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@
*/
package com.svalyn.studio.application.controllers.viewer;

import com.svalyn.studio.domain.account.AccountRole;
import jakarta.validation.constraints.NotNull;

/**
* A viewer of the application.
*
* @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) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -41,22 +53,63 @@ 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<Viewer> 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
@Transactional(readOnly = true)
public Optional<ProfileDTO> 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<AccountDTO> 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<Account> failure) {
payload = new ErrorPayload(input.id(), failure.message());
} else if (result instanceof Success<Account> success) {
payload = new CreateAccountSuccessPayload(input.id(), this.toDTO(success.data()));
}

return payload;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,4 +39,8 @@ public interface IAccountService {
Optional<Viewer> findViewerById(UUID id);

Optional<ProfileDTO> findProfileByUsername(String username);

Page<AccountDTO> findAll(int page, int rowsPerPage);

IPayload createAccount(CreateAccountInput input);
}
Loading

0 comments on commit 77c90d8

Please sign in to comment.