API учетной записи Spring Boot с интеграцией React Frontend и Identicon

Недавно я изучал React и Spring Boot и был бы очень признателен за ваши отзывы о разработанном мной практическом приложении. Приложение состоит из бэкэнда Spring Boot, использующего базу данных H2, а также внешнего интерфейса ReactJS, реализованного на TypeScript. Эти слои взаимодействуют друг с другом через REST API.

API представляет собой базовую систему учетных записей, включающую такие поля, как имя пользователя, пароль и пол. Он также имеет некоторую внутреннюю проверку для обеспечения целостности данных. Также для каждого пользователя создается уникальный идентификатор.

Процесс создания идентификатора в приложении состоит из двух этапов. Во-первых, библиотека Jdenticon используется для создания SVG-файла, представляющего уникальный идентификатор на основе заданного имени пользователя. Затем этот файл SVG преобразуется в изображение PNG с помощью Apache Batik PNGTranscoder, в результате чего изображение представляется в виде массива байтов. Серверная часть предоставляет этот массив байтов в качестве ответа, когда внешний интерфейс запрашивает идентификатор, а внешний интерфейс использует его как большой двоичный объект для отображения изображения идентификатора с использованием элемента img с URL-адресом большого двоичного объекта.

Обратите внимание, что код довольно длинный из-за наличия двух отдельных кодовых баз.

Вот контроллер REST:

@RestController
@CrossOrigin
@RequestMapping("/accounts")
public class AccountController {

    private final AccountService accountService;

    @Autowired
    public AccountController(AccountService accountService) {
        this.accountService = accountService;
    }

    @GetMapping
    public ResponseEntity<List<Account>> getAccounts() {
        try {
            List<Account> accounts = accountService.getAccounts();

            return ResponseEntity.ok(accounts);
        } catch (Exception ex) {
            return ResponseEntity.internalServerError().build();
        }
    }

    @GetMapping("/identicon/{username}")
    public ResponseEntity<byte[]> getIdenticon(@PathVariable String username) {
        try {
            byte[] identiconOutput = accountService.getIdenticon(username);

            return ResponseEntity.ok(identiconOutput);
        } catch (Exception ex) {
            return ResponseEntity.internalServerError().build();
        }
    }

    @PostMapping
    public ResponseEntity<?> createAccount(@RequestBody Account account) {
        try {
            accountService.registerAccount(account);
            return ResponseEntity.ok().build();
        } catch (AccountValidatorException ex) {
            return ResponseEntity.badRequest().build();
        } catch (Exception ex) {
            return ResponseEntity.internalServerError().build();
        }
    }

    @DeleteMapping("/{username}")
    public ResponseEntity<?> deleteAccount(@PathVariable String username) {
        try {
            accountService.deleteAccount(username);

            return ResponseEntity.ok().build();
        } catch (Exception ex) {
            return ResponseEntity.internalServerError().build();
        }
    }
}

Вот сервис аккаунта:

@Service
public class AccountService {

    private final AccountRepository accountRepository;
    private final List<AccountValidatorService> accountValidatorServices;

    @Autowired
    public AccountService(AccountRepository accountRepository, List<AccountValidatorService> accountValidatorServices) {
        this.accountRepository = accountRepository;
        this.accountValidatorServices = accountValidatorServices;
    }

    public List<Account> getAccounts() {
        return accountRepository.getAccounts();
    }

    public void registerAccount(Account account) throws AccountValidatorException {
        List<String> messages = new ArrayList<>();

        for (AccountValidatorService accountValidatorService : accountValidatorServices) {
            if (!accountValidatorService.validate(account)) {
                messages.add(accountValidatorService.getErrorMessage());
            }
        }

        if (!messages.isEmpty()) {
            throw new AccountValidatorException("Error trying to register account: " + messages);
        }

        accountRepository.addAccount(account.getUsername(), account.getPassword(), account.getGender());
    }

    public byte[] getIdenticon(String username) throws TranscoderException, IOException {
        // get the original SVG XML file which will be converted to a PNG with Apache Batik
        String svg = Jdenticon.toSvg(username);

        TranscoderInput input = new TranscoderInput(new StringReader(svg));

        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        TranscoderOutput output = new TranscoderOutput(byteStream);

        PNGTranscoder transcoder = new PNGTranscoder();
        transcoder.transcode(input, output);

        byteStream.flush();
        byteStream.close();

        return byteStream.toByteArray();
    }

    public void deleteAccount(String username) {
        accountRepository.deleteAccountByUsername(username);
    }
}

Вот репозиторий аккаунта:

public interface AccountRepository extends CrudRepository<Account, Long> {

    @Query("SELECT * FROM account")
    List<Account> getAccounts();

    @Modifying
    @Query("DELETE FROM account WHERE username = :username")
    void deleteAccountByUsername(String username);

    @Modifying
    @Query("INSERT INTO account VALUES(NULL, :username, :password, :gender)")
    void addAccount(String username, String password, Gender gender);
}

Вот валидаторы аккаунта:

public interface AccountValidatorService {

    String getErrorMessage();

    boolean validate(Account account);
}

@Service
public class PasswordValidatorService implements AccountValidatorService {

    @Override
    public String getErrorMessage() {
        return "Password must contain at least 3 digits, 3 letters, and 3 special characters";
    }

    @Override
    public boolean validate(Account account) {
        String password = account.getPassword();

        long digitCount = password
            .chars()
            .filter(Character::isDigit)
            .count();

        long letterCount = password
            .chars()
            .filter(Character::isLetter)
            .count();

        long specialCharacterCount = 0;
        Set<Character> specialCharacters = Set.of('!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '-');

        for (Character c : password.toCharArray()) {
            if (specialCharacters.contains(c)) {
                ++specialCharacterCount;
            }
        }

        return digitCount >= 3 && letterCount >= 3 && specialCharacterCount >= 3;
    }
}

@Service
public class UniqueUsernameValidatorService implements AccountValidatorService {

    private final AccountRepository accountRepository;

    @Autowired
    public UniqueUsernameValidatorService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Override
    public String getErrorMessage() {
        return "An account with this username already exists";
    }

    public boolean validate(String username) {
        return accountRepository
            .getAccounts()
            .stream()
            .noneMatch(_account -> _account.getUsername().equals(username));
    }

    @Override
    public boolean validate(Account account) {
        return validate(account.getUsername());
    }
}

@Service
public class UsernameLengthValidatorService implements AccountValidatorService {

    @Override
    public String getErrorMessage() {
        return "Username must have a length larger than 3 and smaller than or equal to 50";
    }

    @Override
    public boolean validate(Account account) {
        String username = account.getUsername();

        return username.length() >= 3 && username.length() <= 50;
    }
}

Реагировать интерфейс:

import { Button, Flex, Input, Select, Text } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';

type Gender="MALE" | 'FEMALE'

interface Account {
    username: string
    password: string
    gender: Gender
}

class API {
    private constructor() { }

    static readonly endpoint="http://localhost:8080/accounts"

    static async createAccount(account: Account): Promise<void> {
        await fetch(this.endpoint, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(account)
        })
    }

    static async getIdenticon(username: string): Promise<Blob> {
        const result = await fetch(`${this.endpoint}/identicon/${username}`)

        const blob = result.blob()

        return blob
    }

    static async getAccounts(): Promise<Account[]> {
        const result = await fetch(this.endpoint)
        const json: Promise<Account[]> = await result.json()
        
        return json
    }

    static async deleteAccount(account: Account): Promise<void> {
        await fetch(`${this.endpoint}/${account.username}`, {
            method: 'DELETE',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(account)
        })
    }
} 

type AccountDeleteAction = (account: Account) => void

interface AccountListItemProps {
    account: Account
    onDeleteClick: AccountDeleteAction
}

function AccountListItem(props: AccountListItemProps): JSX.Element {
    const [imageUrl, setImageUrl] = useState<string | null>(null)

    async function getIdenticon(): Promise<void> {
        const response = await API.getIdenticon(props.account.username)

        const blobUrl = URL.createObjectURL(response)

        setImageUrl(blobUrl)
    }

    if (imageUrl === null) {
        getIdenticon()
    }

    return (
        <Flex
            shadow='md' 
            borderRadius="md"
            width="100%"
            alignItems="center"
            paddingRight="16px"
        >
            <Flex flexDirection='row' width="100%" padding='16px' height="100px">
                {imageUrl && <img src={imageUrl} />}
                <Flex flexDirection='column' justifyContent="center" marginLeft="16px">
                    <h1><Text as="span" fontWeight="bold">Username:</Text> {props.account.username}</h1>
                    <h2><Text as="span" fontWeight="bold">Password:</Text> {props.account.password}</h2>
                    <h3><Text as="span" fontWeight="bold">Gender:</Text> {props.account.gender}</h3>
                </Flex>
            </Flex>

            <Flex marginLeft="auto">
                <Button colorScheme="red" onClick={() => props.onDeleteClick(props.account)}>Delete</Button>
            </Flex>
        </Flex>
    )
}

interface AccountListProps {
    accounts: Account[]
    onDeleteClick: AccountDeleteAction
}

function AccountList(props: AccountListProps): JSX.Element {
    return (
        <Flex flexDirection='column' width="100%">
            {props.accounts.map((account, i) => 
                <AccountListItem 
                    key={i}
                    account={account} 
                    onDeleteClick={(account) => props.onDeleteClick(account)} 
                />
            )}
        </Flex>
    )
}

function App(): JSX.Element {
    const [accounts, setAccounts] = useState<Account[]>([])

    const [username, setUsername] = useState<string | null>(null)
    const [password, setPassword] = useState<string | null>(null)
    const [gender, setGender] = useState<Gender | null>(null)

    const [isLoading, setIsLoading] = useState(true)

    async function fetchAccounts(): Promise<void> {
        const data = await API.getAccounts()


        setAccounts(data)
        setIsLoading(false)
    }

    async function formSubmitHandler(e: React.FormEvent<HTMLFormElement>): Promise<void> {
        e.preventDefault()

        if (username === null || password === null || gender === null) {
            return
        }

        const account: Account = { 
            username: username,
            password: password,
            gender: gender
        }

        setIsLoading(true)

        await API.createAccount(account)

        setUsername(null)
        setPassword(null)
        setGender(null)

        await fetchAccounts()

        setIsLoading(false)
    }

    useEffect(() => {
        fetchAccounts()
    }, [])

    return (
        <Flex padding='16px' height="100vhw" flexDirection='column'>
            <Text fontSize="32px" marginBottom='8px'>Create Account</Text>

            <Flex 
                flexDirection='column' 
                padding='16px' 
                shadow='md' 
                borderRadius="md" 
            >
                <form onSubmit={formSubmitHandler}>
                    <Flex flexDirection='column' gap='8px'>
                        <Input placeholder="Username" value={username ?? ''} onChange={(e) => setUsername(e.target.value)} />
                        <Input placeholder="Password" type="password" value={password ?? ''} onChange={(e) => setPassword(e.target.value)} />

                        <Select
                            value={gender === null ? 0 : gender}
                            placeholder="Select Gender"
                            onChange={(e) => setGender(e.target.value as Gender)}
                        >
                            <option value="MALE">Male</option>
                            <option value="FEMALE">Female</option>
                        </Select>

                        <Button type="submit" alignSelf="flex-start">Add Account</Button>
                    </Flex>
                </form>
            </Flex>

            {isLoading && <Flex width="100%" flexGrow={1} alignItems="center" justifyContent="center"><h1>Loading...</h1></Flex>}

            <AccountList onDeleteClick={async (account) => {
                await API.deleteAccount(account)

                fetchAccounts()
            }} accounts={accounts} />
        </Flex>
    )
}

export default App;

Вот результат:

введите описание изображения здесь

Я также понимаю все последствия для безопасности, поэтому хочу повторить, что это всего лишь практика 🙂

Буду признателен за отзывы по поводу:

  1. Общий дизайн и архитектура: Я хотел бы знать ваше мнение об общем дизайне и архитектуре приложения. Существуют ли какие-либо шаблоны проектирования или архитектурные принципы, которые можно было бы улучшить или использовать более эффективно?

  2. Организация и структура кода: Пожалуйста, ознакомьтесь с организацией и структурой кодовой базы. Есть ли области, в которых код можно было бы лучше организовать или разделить на модули? Существуют ли какие-либо соглашения об именах, которые можно было бы улучшить?

  3. Бэкенд реализация: Я хотел бы получить отзыв о реализации бэкэнда с использованием Spring Boot. Есть ли области, где код можно оптимизировать или упростить? Есть ли потенциальные проблемы с производительностью или масштабируемостью?

  4. Реализация внешнего интерфейса: Оставьте отзыв о внешнем интерфейсе React, реализованном на TypeScript. Существуют ли какие-либо передовые методы или оптимизации, которые можно было бы применить для улучшения внешнего кода? Как пользовательский интерфейс и общий пользовательский опыт?

  5. Дизайн REST API: Ознакомьтесь с конечными точками REST API и их функциональностью. Можно ли внести какие-либо улучшения для повышения удобства использования и эффективности API? Есть ли какие-либо проблемы безопасности, которые необходимо решить?

  6. Проверка данных и обработка ошибок: Оцените механизмы проверки данных и обработки ошибок, реализованные в серверной части. Существуют ли какие-либо потенциальные уязвимости или крайние случаи, которые необходимо обрабатывать более эффективно?

С уважением.

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *