Skip to content

Solution#251

Open
shatskov-artur wants to merge 3 commits intomate-academy:masterfrom
shatskov-artur:develop
Open

Solution#251
shatskov-artur wants to merge 3 commits intomate-academy:masterfrom
shatskov-artur:develop

Conversation

@shatskov-artur
Copy link

Solution

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This is an excellent submission that implements nearly all the required features for a full-stack authentication system. I am requesting a few changes to meet all the requirements.

The main issue is that the application does not redirect the user to their Profile page after account activation. Currently, it only shows a success message with a link to log in. To fix this, the backend's activate function in src/controllers/authController.js should create a session for the user, and the frontend's Activate.jsx component should use this to log the user in and redirect them to /profile.

Additionally, please consider these improvements:

  • Add error handling to the fetch call in client/src/pages/ForgotPassword.jsx to handle cases where the request fails.
  • Refactor the Profile.jsx component to use the user data from AuthContext instead of fetching it again.

You've done a great job, and with these adjustments, the project will be complete.


✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨

Important

Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.

Tip

If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.

Caution

Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.

Footnotes

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

client/README.md Outdated
Comment on lines +1 to +16
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)

## React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).

## Expanding the ESLint configuration

If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This README is the default one from the Vite template. It's a good practice to replace it with a description of your own project. Consider adding information about what the application does, how to install dependencies, and how to run it locally.

Comment on lines +1 to +184
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;

&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}

.hero {
position: relative;

.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}

.base {
width: 170px;
position: relative;
z-index: 0;
}

.framework,
.vite {
position: absolute;
}

.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}

.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}

#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;

@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}

#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;

& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}

.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}

@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}

#docs {
border-right: 1px solid var(--border);

@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}

#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;

.logo {
height: 18px;
}

a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;

&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}

@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;

li {
flex: 1 1 calc(50% - 8px);
}

a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}

#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}

.ticks {
position: relative;
width: 100%;

&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}

&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These styles seem to be part of the default Vite template and are not used in your application. It's best to remove this file to keep the codebase clean.

<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>client</title>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good practice to set a more descriptive title for your application. Consider changing this from "client" to something like "Authentication App" to better reflect the page's content.

Comment on lines +27 to +32
{status === 'success' ? (
<>
<h1>Account activated!</h1>
<div className="alert alert-success">{message}</div>
<div className="links"><Link to="/login">Log in</Link></div>
</>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task description requires that the user be redirected to the Profile page after successful activation. Currently, you are displaying a link to the login page, which requires an extra step from the user. To implement the redirect, you would need to automatically log the user in after they activate their account and then navigate them to the /profile page. This would likely require the backend to create a session and return the user data upon successful activation.

Comment on lines +11 to +16
await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email }),
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation doesn't handle potential errors from the fetch call. If the server returns an error status (e.g., 500), or if there's a network issue, setSent(true) will still be executed, misleading the user into thinking the request was successful. You should check the response status (e.g., res.ok) and wrap the call in a try...catch block to handle network failures, similar to how you've done in other components like Login.jsx.

Comment on lines +32 to +39
useEffect(() => {
fetch('/api/profile', { credentials: 'include' })
.then((res) => res.json())
.then((data) => {
setUser(data.user);
setNameField(data.user.name);
});
}, []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect duplicates the functionality that already exists in AuthContext.jsx. The AuthContext fetches the user's data when the application loads, making it available globally via the useAuth hook.

Fetching the data again here is unnecessary and introduces a separate local user state that could potentially become out of sync with the global state.

Consider refactoring this component to use the user object directly from the context:

  1. Remove this useEffect and the local [user, setUser] state.
  2. Get the user object from the context: const { user, login } = useAuth();
  3. Initialize the nameField state based on the user from the context.

This change will make AuthContext the single source of truth for user data, simplify your component, and remove a redundant API call.

Comment on lines +89 to +105
async function activate(req, res) {
const token = await Token.findOne({
where: {
id: req.params.token,
type: 'activation',
expiresAt: { [Op.gt]: new Date() },
},
});

if (!token) {
return res.status(400).json({ error: 'This activation link is invalid or has expired.' });
}

await User.update({ isActive: true }, { where: { id: token.userId } });
await token.destroy();

res.json({ message: 'Your account has been activated! You can now log in.' });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task requires redirecting the user to their Profile page after activation. To enable this, the backend should automatically log the user in upon successful activation. Currently, you only return a success message.

Consider finding the user associated with the token, creating a session for them (e.g., req.session.userId = token.userId;), and returning the user data in the JSON response, similar to what you do in the login function. This will allow the frontend to update its auth state and perform the required redirect.

Comment on lines +9 to +27
function validatePassword(password) {
if (password.length < 8) {
return 'Password must be at least 8 characters long.';
}

if (!/[A-Z]/.test(password)) {
return 'Password must contain at least one uppercase letter.';
}

if (!/[a-z]/.test(password)) {
return 'Password must contain at least one lowercase letter.';
}

if (!/[0-9]/.test(password)) {
return 'Password must contain at least one digit.';
}

return null;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validatePassword function is identical to the one in authController.js. To avoid duplicating code and ensure consistency, it's a good practice to move shared functions like this to a common utility file (e.g., src/utils/validation.js). You could then import and use it in both controllers.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Great work on the backend implementation! However, this submission requires changes because a core requirement for account activation has not been met on the frontend.

Required Change:

  • In client/src/pages/Activate.jsx, after a user successfully activates their account, they are not automatically logged in and redirected to their profile page. The backend now correctly creates a session and returns user data. Please update the component to use the login function from your useAuth context with this data and then navigate the user to /profile.

Suggestions for Improvement:

  • Profile.jsx: This component makes a redundant API call for user data that is already available in the AuthContext. Refactoring to get the user object from useAuth() will improve performance.
  • ForgotPassword.jsx: The fetch call is not wrapped in a try...catch block, which can lead to unhandled errors if the network request fails.

You're very close to completing the task. Keep up the excellent effort!


✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨

Important

Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.

Tip

If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.

Caution

Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.

Footnotes

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

client/README.md Outdated
Comment on lines +1 to +16
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)

## React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).

## Expanding the ESLint configuration

If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This README is the default one from the Vite template. It would be beneficial to add a description of your project, including its features and how to run it locally. Good documentation is a key part of software development.

Comment on lines +1 to +184
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;

&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}

.hero {
position: relative;

.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}

.base {
width: 170px;
position: relative;
z-index: 0;
}

.framework,
.vite {
position: absolute;
}

.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}

.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}

#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;

@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}

#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;

& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}

.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}

@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}

#docs {
border-right: 1px solid var(--border);

@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}

#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;

.logo {
height: 18px;
}

a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;

&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}

@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;

li {
flex: 1 1 calc(50% - 8px);
}

a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}

#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}

.ticks {
position: relative;
width: 100%;

&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}

&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These styles seem to be part of the default Vite starter template and are not used in your application. Consider removing them to reduce code clutter and keep the project focused on the required features.

<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>client</title>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good practice to set a descriptive title for your application. This title appears in the browser tab and search engine results. Consider changing it to something like "Auth App".

Comment on lines +16 to +19
} else {
setStatus('success');
setMessage(data.message);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task requires redirecting the user to their profile page after successful activation. The backend now returns the user data and creates a session, which is great.

Here in the frontend, you should now use the login function from useAuth() to set the user in the context and then use useNavigate() to redirect them to /profile. This will complete the automatic login flow.

Comment on lines +11 to +16
await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email }),
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch call here can fail due to network issues or if the server is down. Without a try...catch block, this will result in an unhandled promise rejection and a poor user experience. It's important to wrap asynchronous requests like this in error handling blocks.

Comment on lines +32 to +39
useEffect(() => {
fetch('/api/profile', { credentials: 'include' })
.then((res) => res.json())
.then((data) => {
setUser(data.user);
setNameField(data.user.name);
});
}, []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect hook duplicates the logic already present in AuthContext for fetching user data. The user information is available globally via useAuth().

Refactoring this component to use the context's user object directly (e.g., const { user } = useAuth()) will remove this redundant network call, eliminate the need for the local user state on line 21, and make the component more efficient and easier to maintain.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Excellent work on this project! You've successfully addressed all the critical feedback from the previous review, and the application's core functionality is now complete and working well.

I am approving your submission as an educational override so you can move forward, but there is one important area for improvement. Several of your frontend components are missing error handling for network requests in files like Login.jsx, Profile.jsx, and Register.jsx. If a fetch call fails due to a network issue, it could cause an unhandled error and crash the application. To make your code more robust, it's best practice to wrap asynchronous API calls in try...catch blocks.

This is a key concept for building resilient applications. Overall, you've done a fantastic job building a secure and functional full-stack application. Keep up the great work!


✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨

Footnotes

  1. Rate AI review example

Comment on lines +10 to +20
fetch(`/api/profile/confirm-email-change/${token}`, { credentials: 'include' })
.then((res) => res.json())
.then((data) => {
if (data.error) {
setStatus('error');
setMessage(data.error);
} else {
setStatus('success');
setMessage(data.message);
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fetch call is missing a .catch() block. If the network request fails for reasons like a server outage or no internet connection, the promise will be rejected, leading to an unhandled error. It's good practice to add a .catch() to handle such scenarios gracefully.

Comment on lines +13 to +23
fetch(`/api/auth/activate/${token}`, { method: 'POST', credentials: 'include' })
.then((res) => res.json())
.then((data) => {
if (data.error) {
setStatus('error');
setMessage(data.error);
} else {
login(data.user);
navigate('/profile');
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fetch promise chain is missing a .catch() block. If the network request fails (e.g., no internet connection) or if the server returns a non-JSON response (e.g., for a 500 error), the promise will reject and cause an unhandled error in your application. It's a good practice to add a .catch() to handle these potential failures gracefully, for example by setting an error status.

Comment on lines +15 to +34
async function handleSubmit(e) {
e.preventDefault();
setError('');

const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(fields),
});

const data = await res.json();

if (!res.ok) {
return setError(data.error);
}

login(data.user);
navigate('/profile');
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleSubmit function uses async/await but isn't wrapped in a try...catch block. If the fetch call fails due to a network error (e.g., server is offline) or if res.json() fails because the response isn't valid JSON, it will result in an unhandled promise rejection, potentially crashing the application. Consider wrapping the API call logic in a try...catch block to handle these types of errors gracefully.

Comment on lines +31 to +50
async function handleNameSubmit(e) {
e.preventDefault();
setNameMsg({ type: '', text: '' });

const res = await fetch('/api/profile/name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ name: nameField }),
});

const data = await res.json();

if (!res.ok) {
return setNameMsg({ type: 'error', text: data.error });
}

login({ ...user, name: nameField });
setNameMsg({ type: 'success', text: data.message });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is missing a try...catch block. If the fetch call fails due to a network error, it will result in an unhandled promise rejection. It's a good practice to wrap asynchronous operations like this in try...catch to handle potential errors gracefully.

Comment on lines +52 to +71
async function handlePasswordSubmit(e) {
e.preventDefault();
setPasswordMsg({ type: '', text: '' });

const res = await fetch('/api/profile/password', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(passwordFields),
});

const data = await res.json();

if (!res.ok) {
return setPasswordMsg({ type: 'error', text: data.error });
}

setPasswordFields({ oldPassword: '', newPassword: '', confirmation: '' });
setPasswordMsg({ type: 'success', text: data.message });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the name submission handler, this function would benefit from a try...catch block to gracefully handle potential network errors or server issues during the fetch call.

Comment on lines +73 to +92
async function handleEmailSubmit(e) {
e.preventDefault();
setEmailMsg({ type: '', text: '' });

const res = await fetch('/api/profile/email', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(emailFields),
});

const data = await res.json();

if (!res.ok) {
return setEmailMsg({ type: 'error', text: data.error });
}

setEmailFields({ newEmail: '', password: '' });
setEmailMsg({ type: 'success', text: data.message });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could also be made more robust by wrapping the fetch logic in a try...catch block. This ensures that any network-level failures are caught and handled, preventing the application from crashing due to an unhandled promise rejection.

Comment on lines +20 to +39
async function handleSubmit(e) {
e.preventDefault();
setError('');

const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(fields),
});

const data = await res.json();

if (!res.ok) {
return setError(data.error);
}

setSuccess(data.message);
setTimeout(() => navigate('/login'), 2000);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asynchronous function handles the form submission but doesn't have a try...catch block. If the fetch call fails due to a network error (like the server being offline), it will result in an unhandled promise rejection. It's best practice to wrap your API call logic in a try...catch block to handle these errors gracefully and provide feedback to the user.

Comment on lines +15 to +33
async function handleSubmit(e) {
e.preventDefault();
setError('');

const res = await fetch(`/api/auth/reset-password/${token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(fields),
});

const data = await res.json();

if (!res.ok) {
return setError(data.error);
}

setSuccess(true);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This async function is missing a try...catch block. If the fetch call fails due to a network error (e.g., server is offline), it will lead to an unhandled promise rejection. Wrapping the logic in a try...catch block would make the component more robust by allowing you to handle such errors gracefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants