Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User account #43

Merged
merged 6 commits into from
Apr 20, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip
Jovert Lota Palonpon committed Apr 20, 2019

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit c20fc69ce9c0f5b520117f15ae40876c0b3ba80b
33 changes: 31 additions & 2 deletions app/Http/Controllers/Api/V1/Settings/AccountController.php
Original file line number Diff line number Diff line change
@@ -2,12 +2,41 @@

namespace App\Http\Controllers\Api\V1\Settings;

use App\User;
use Hash;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use Illuminate\Validation\ValidationException;

class AccountController extends Controller
{
/**
* Update User's password
*
* @param Illuminate\Http\Request $request
*
* @return Illuminate\Http\JsonResponse
*/
public function updatePassword(Request $request) : JsonResponse
{
$request->validate([
'old_password' => 'required|string',
'password' => 'required|string|confirmed|min:8|pwned:100'
]);

}
$user = auth()->guard('api')->user();

if (! Hash::check($request->input('old_password'), $user->password)) {
throw ValidationException::withMessages([
'old_password' => [trans('auth.password_mismatch')]
]);

return response()->json('Password was not Changed!', 422);
}

$user->password = bcrypt($request->input('password'));
$user->update();

return response()->json('Password Changed!');
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/V1/Settings/ProfileController.php
Original file line number Diff line number Diff line change
@@ -37,4 +37,4 @@ public function update(Request $request) : JsonResponse

return response()->json($user);
}
}
}
334 changes: 319 additions & 15 deletions resources/js/views/__backoffice/settings/Account.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import React, { useState, useEffect } from 'react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';

import { Grid, Hidden, Paper, Typography, withStyles } from '@material-ui/core';
import {
Button,
CircularProgress,
Grid,
Hidden,
IconButton,
InputAdornment,
Paper,
TextField,
Typography,
withStyles,
} from '@material-ui/core';

import {
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
} from '@material-ui/icons';

import * as UrlUtils from '../../../utils/URL';
import {
@@ -11,8 +29,71 @@ import {

function Account(props) {
const { classes, ...other } = props;
const { location } = props;
const { location, pageProps } = props;
const { user } = pageProps;

const [loading, setLoading] = useState(false);
const [message, setMessage] = useState({});
const [formVisible, setFormVisibility] = useState(false);
const [oldPasswordVisible, setOldPasswordVisibility] = useState(false);
const [passwordVisible, setPasswordVisibility] = useState(false);
const [
passwordConfirmationVisible,
setPasswordConfirmationVisibility,
] = useState(false);

/**
* Handle password form submit, this should send an API response
* to update the password.
*
* @param {object} values
* @param {object} form
*
* @return {undefined}
*/
const handlePasswordSubmit = async (
values,
{ setSubmitting, setErrors },
) => {
setSubmitting(false);

const updatePassword = () =>
axios.patch('/api/v1/settings/account/password', values);

try {
setLoading(true);

await updatePassword();

setMessage({
type: 'success',
body: Lang.get('settings.account_password_updated'),
closed: () => setMessage({}),
});

setLoading(false);
} catch (error) {
if (!error.response) {
return;
}

const { errors } = error.response.data;

if (errors) {
setErrors(errors);
} else {
setMessage({
type: 'error',
body: Lang.get('settings.account_password_not_updated'),
closed: () => setMessage({}),
actionText: Lang.get('actions.retry'),
action: async () => await updatePassword(),
});
}

setLoading(false);
}
};

useEffect(() => {
const queryParams = UrlUtils._queryParams(location.search);
@@ -24,10 +105,229 @@ function Account(props) {
}
});

const renderForm = (
const renderPasswordForm = (
<Grid item md={8} sm={12} xs={12}>
<Paper className={classes.form}>
<Typography>Account Page</Typography>
<Formik
initialValues={{
old_password: '',
password: '',
password_confirmation: '',
}}
validationSchema={Yup.object().shape({
old_password: Yup.string().required(
Lang.get('validation.required', {
attribute: 'old password',
}),
),

password: Yup.string()
.required(
Lang.get('validation.required', {
attribute: 'password',
}),
)
.min(
8,
Lang.get('validation.min.string', {
attribute: 'password',
min: 8,
}),
)
.oneOf(
[Yup.ref('password_confirmation'), null],
Lang.get('validation.confirmed', {
attribute: 'password',
}),
),

password_confirmation: Yup.string()
.required(
Lang.get('validation.required', {
attribute: 'password confirmation',
}),
)
.min(
8,
Lang.get('validation.min.string', {
attribute: 'password confirmation',
min: 8,
}),
),
})}
onSubmit={handlePasswordSubmit}
validateOnBlur={false}
>
{({
values,
errors,
submitCount,
isSubmitting,
handleChange,
}) => (
<Form>
<Typography variant="h6" gutterBottom>
Change Password
</Typography>

<TextField
type={oldPasswordVisible ? 'text' : 'password'}
id="old_password"
name="old_password"
label="Old Password"
placeholder="Enter your old password"
value={values.old_password}
onChange={handleChange}
fullWidth
margin="dense"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Toggle old password visibility"
onClick={() =>
setOldPasswordVisibility(
!oldPasswordVisible,
)
}
>
{oldPasswordVisible ? (
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)}
</IconButton>
</InputAdornment>
),
}}
error={
submitCount > 0 &&
errors.hasOwnProperty('old_password')
}
helperText={
submitCount > 0 &&
errors.hasOwnProperty('old_password') &&
errors.old_password
}
/>

<TextField
type={passwordVisible ? 'text' : 'password'}
id="password"
name="password"
label="Password"
placeholder="Enter your password"
value={values.password}
onChange={handleChange}
fullWidth
margin="dense"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={() =>
setPasswordVisibility(
!passwordVisible,
)
}
>
{passwordVisible ? (
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)}
</IconButton>
</InputAdornment>
),
}}
error={
submitCount > 0 &&
errors.hasOwnProperty('password')
}
helperText={
submitCount > 0 &&
errors.hasOwnProperty('password') &&
errors.password
}
/>

<TextField
type={
passwordConfirmationVisible
? 'text'
: 'password'
}
id="password_confirmation"
name="password_confirmation"
label="Password Confirmation"
placeholder="Enter your password again"
value={values.password_confirmation}
onChange={handleChange}
fullWidth
margin="dense"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password confirmation visibility"
onClick={() =>
setPasswordConfirmationVisibility(
!passwordConfirmationVisible,
)
}
>
{passwordConfirmationVisible ? (
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)}
</IconButton>
</InputAdornment>
),
}}
error={
submitCount > 0 &&
errors.hasOwnProperty(
'password_confirmation',
)
}
helperText={
submitCount > 0 &&
errors.hasOwnProperty(
'password_confirmation',
) &&
errors.password_confirmation
}
/>

<div className={classes.dense} />

<Button
type="submit"
variant="contained"
color="primary"
disabled={
(errors &&
Object.keys(errors).length > 0 &&
submitCount > 0) ||
isSubmitting
}
>
{loading && (
<div className={classes.spinner}>
<CircularProgress
size={14}
color="inherit"
/>
</div>
)}

<Typography color="inherit">Change</Typography>
</Button>
</Form>
)}
</Formik>
</Paper>
</Grid>
);
@@ -40,24 +340,20 @@ function Account(props) {
setFormVisibility: () => setFormVisibility(!formVisible),
}}
>
<Hidden mdUp>{formVisible && renderForm}</Hidden>
<Hidden mdUp>{formVisible && <>{renderPasswordForm}</>}</Hidden>

<Hidden smDown>{renderForm}</Hidden>
<Hidden smDown>
<>{renderPasswordForm}</>
</Hidden>
</SettingsLayout>
);

if (formVisible) {
return (
<SlaveLayout {...other} pageTitle="Account" loading={false}>
{renderBody}
</SlaveLayout>
);
}
const Layout = formVisible ? SlaveLayout : CleanLayout;

return (
<CleanLayout {...other} pageTitle="Account" loading={false}>
<Layout {...other} pageTitle="Account" message={message}>
{renderBody}
</CleanLayout>
</Layout>
);
}

@@ -66,6 +362,14 @@ const styles = theme => ({
padding: theme.spacing.unit * 3,
minHeight: '100%',
},

dense: {
marginTop: 16,
},

spinner: {
marginRight: theme.spacing.unit,
},
});

export default withStyles(styles)(Account);
26 changes: 18 additions & 8 deletions resources/js/views/__backoffice/settings/Profile.js
Original file line number Diff line number Diff line change
@@ -47,7 +47,7 @@ function Profile(props) {
*
* @return {undefined}
*/
const handleSubmit = async (values, { setSubmitting }) => {
const handleSubmit = async (values, { setSubmitting, setErrors }) => {
setSubmitting(false);

const updateProfile = () =>
@@ -66,13 +66,23 @@ function Profile(props) {

setLoading(false);
} catch (error) {
setMessage({
type: 'error',
body: Lang.get('settings.profile_not_updated'),
closed: () => setMessage({}),
actionText: Lang.get('actions.retry'),
action: async () => await updateProfile(),
});
if (!error.response) {
return;
}

const { errors } = error.response.data;

if (errors) {
setErrors(errors);
} else {
setMessage({
type: 'error',
body: Lang.get('settings.profile_not_updated'),
closed: () => setMessage({}),
actionText: Lang.get('actions.retry'),
action: async () => await updateProfile(),
});
}

setLoading(false);
}
1 change: 1 addition & 0 deletions resources/lang/en/auth.php
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
|
*/

'password_mismatch' => 'The password you entered is incorrect.',
'failed' => "Sorry that didn't work. Please try again.",
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',

6 changes: 3 additions & 3 deletions resources/lang/en/settings.php
Original file line number Diff line number Diff line change
@@ -13,6 +13,6 @@

'profile_updated' => 'Profile successfully updated!',
'profile_not_updated' => 'Profile was not updated!',
'account_updated' => 'Account successfully updated!',
'account_not_updated' => 'Account was not updated!',
];
'account_password_updated' => 'Account Password successfully updated!',
'account_password_not_updated' => 'Account Password was not updated!',
];
1 change: 1 addition & 0 deletions resources/lang/fil/auth.php
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
|
*/

'password_mismatch' => 'Ang password na iyong nailagay ay mali.',
'failed' => 'Paumanhin na hindi ito gumagana. Ulitin mo ulit.',
'throttle' =>
'Masyadong maraming mga pagtatangka sa pag-login. Pakisubukang muli pagkatapos ng :seconds segundo.',
7 changes: 4 additions & 3 deletions resources/lang/fil/settings.php
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@

'profile_updated' => 'Ang iyong Profile ay nabago!',
'profile_not_updated' => 'Ang iyong Profile ay hindi nabago!',
'account_updated' => 'Ang iyong Account ay nabago!',
'account_not_updated' => 'Ang iyong Account ay hindi nabago!',
];
'account_password_updated' => 'Ang Password ng iyong Account ay nabago!',
'account_password_not_updated' =>
'Ang Password ng iyong Account ay hindi nabago!',
];
4 changes: 2 additions & 2 deletions routes/api.php
Original file line number Diff line number Diff line change
@@ -30,10 +30,10 @@
});

Route::middleware('auth:api')->group(function () {
Route::namespace('Settings')->prefix('settings')->name('settings.')->group(function() {
Route::namespace('Settings')->prefix('settings')->name('settings.')->group(function () {
Route::patch('profile', 'ProfileController@update')->name('profile');

Route::prefix('account')->name('account.')->group(function() {
Route::prefix('account')->name('account.')->group(function () {
Route::patch('password', 'AccountController@updatePassword')->name('password');
});
});
22 changes: 20 additions & 2 deletions tests/Feature/Api/V1/Settings/AccountTest.php
Original file line number Diff line number Diff line change
@@ -7,5 +7,23 @@

class AccountTest extends BaseTest
{
//
}
/** @test */
public function a_user_can_change_its_password()
{
$attributes = [
'old_password' => 'secret',
'password' => 'tellmewhat',
'password_confirmation' => 'tellmewhat'
];

// The response body that should be sent alongside the request.
$body = array_merge($this->getDefaultPayload(), $attributes);

// Assuming that the user's password has been updated,
// It must return a 200 response status and then,
// It must return a response body containing the text: `Password Changed!`
$this->patch(route('api.v1.settings.account.password'), $body)
->assertStatus(200)
->assertSee('Password Changed!');
}
}
2 changes: 1 addition & 1 deletion tests/Feature/Api/V1/Settings/ProfileTest.php
Original file line number Diff line number Diff line change
@@ -35,4 +35,4 @@ public function a_user_can_update_its_profile()
->assertStatus(200)
->assertJsonFragment($attributes);
}
}
}