forget password

This commit is contained in:
pajri
2022-12-29 11:38:55 +07:00
parent 0781e1ea00
commit 40ad4a22c7
27 changed files with 721 additions and 15 deletions

View File

@@ -30,7 +30,7 @@ class SendVerifyEmail extends Mailable
public function build()
{
$this->subject('Verify Email')
->markdown('email_user');
->markdown('verify_email');
return $this;
}

View File

@@ -4,6 +4,8 @@ namespace Modules\Internal\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Crypt;
use Error;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
@@ -87,23 +89,25 @@ class AuthController extends Controller
return response(['message' => 'User Tidak Ditemukan'], 404);
}
Event(new ForgetPassword($user));
Mail::to($user->email)->send(new SendVerifyEmail($user));
// Mail::to($user->email)->send(new SendVerifyEmail($user));
return response()->json($user);
}
public function forgetPassword(Request $request)
{
return $request->all();
$request->validate([
'email' => 'required|email',
'new_password' => 'required',
'confirm_new_password' => 'required'
]);
$token = Crypt::decryptString($request->token);
$email = explode('|', $token)[0];
$user = User::query()
->where('email', $request->email)
->where('email', $email)
->first();
if (!$user) {
@@ -113,7 +117,7 @@ class AuthController extends Controller
if ($request["new_password"] != $request["confirm_new_password"]) {
return response([
'message' => "Password Tidak Sama"
]);
], 404);
}
$user->update([

View File

@@ -2,10 +2,12 @@
namespace Modules\Internal\Listeners;
use App\Models\User;
use Modules\Internal\Events\ForgetPassword;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
use Modules\Internal\Notifications\NotifyVerifyEmail;
class SendVerifyEmail
{
@@ -27,7 +29,9 @@ class SendVerifyEmail
*/
public function handle(ForgetPassword $event)
{
dd($event->data);
Mail::raw('Hello World!', function($msg) {$msg->to('myemail@gmail.com')->subject('Test Email'); });
User::where('email', $event->data['email'])
->each(function ($user) use ($event) {
$user->notify(new NotifyVerifyEmail($event->data));
});
}
}

View File

@@ -2,6 +2,7 @@
namespace Modules\Internal\Notifications;
use Crypt;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -10,15 +11,15 @@ use Illuminate\Notifications\Messages\MailMessage;
class NotifyVerifyEmail extends Notification
{
use Queueable;
protected $data;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
public function __construct($data)
{
//
$this->data = $data;
}
/**
@@ -40,10 +41,18 @@ class NotifyVerifyEmail extends Notification
*/
public function toMail($notifiable)
{
$token = Crypt::encryptString($this->data['email'] . '|' . now());
$url = env('INTERNAL_URL', 'https://aso.linksehat.com') . '/auth/forget-password?token=' . $token;
return (new MailMessage)
->line('The introduction to the notification.')
->action('Notification Action', 'https://laravel.com')
->line('Thank you for using our application!');
->subject('Verify Email')
->markdown('verify_email', ['url' => $url]);
// return (new MailMessage)
// ->line('The introduction to the notification.')
// ->action('Notification Action', 'https://laravel.com')
// ->line('Thank you for using our application!');
}
/**

View File

@@ -0,0 +1,61 @@
import { Link as RouterLink } from 'react-router-dom';
// @mui
import { styled } from '@mui/material/styles';
import { Box, Button, Link, Container, Typography } from '@mui/material';
// layouts
import LogoOnlyLayout from '../../layouts/LogoOnlyLayout';
// routes
import { PATH_AUTH } from '../../routes/paths';
// components
import Page from '../../components/Page';
import Iconify from '../../components/Iconify';
// sections
import { ForgetPasswordForm } from '../../sections/auth/forget-password';
import { useSearchParams } from 'react-router-dom';
// ----------------------------------------------------------------------
const RootStyle = styled('div')(({ theme }) => ({
display: 'flex',
height: '100%',
alignItems: 'center',
padding: theme.spacing(12, 0),
}));
// ----------------------------------------------------------------------
export default function ForgetPassword() {
const [searchParams, setSearchParams] = useSearchParams();
const token = searchParams.get('token');
return (
<Page title="Verify" sx={{ height: 1 }}>
<RootStyle>
<LogoOnlyLayout />
<Container>
<Box sx={{ maxWidth: 480, mx: 'auto' }}>
<Button
size="small"
component={RouterLink}
to={PATH_AUTH.login}
startIcon={<Iconify icon={'eva:arrow-ios-back-fill'} width={20} height={20} />}
sx={{ mb: 3 }}
>
Back
</Button>
<Typography variant="h3" paragraph></Typography>
<Typography sx={{ color: 'text.secondary' }}>
Please enter your new password.
</Typography>
<Box sx={{ mt: 5, mb: 3 }}>
<ForgetPasswordForm token={token} />
</Box>
</Box>
</Container>
</RootStyle>
</Page>
);
}

View File

@@ -53,6 +53,7 @@ export default function Router() {
// { path: 'login-unprotected', element: <Login /> },
// { path: 'register-unprotected', element: <Register /> },
{ path: 'reset-password', element: <ResetPassword /> },
{ path: 'forget-password', element: <ForgetPassword /> },
// { path: 'verify', element: <VerifyCode /> },
],
},
@@ -269,6 +270,7 @@ export default function Router() {
const Login = Loadable(lazy(() => import('../pages/auth/Login')));
const ResetPassword = Loadable(lazy(() => import('../pages/auth/ResetPassword')));
const ForgetPassword = Loadable(lazy(() => import('../pages/auth/ForgetPassword')));
// Dashboard
const Dashboard = Loadable(lazy(() => import('../pages/Dashboard')));

View File

@@ -0,0 +1,117 @@
import * as Yup from 'yup';
// form
import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
// @mui
import { Alert, IconButton, InputAdornment, Stack, Typography } from '@mui/material';
import { LoadingButton } from '@mui/lab';
// hooks
import useIsMountedRef from '../../../hooks/useIsMountedRef';
// components
import { FormProvider, RHFTextField } from '../../../components/hook-form';
import axios from '../../../utils/axios';
import Iconify from '../../../components/Iconify';
// ----------------------------------------------------------------------
type FormValuesProps = {
email: string;
afterSubmit?: string;
};
type Props = {
token: string;
};
export default function ForgetPasswordForm({ token }: Props) {
const isMountedRef = useIsMountedRef();
const navigate = useNavigate();
const [showPasswordNew, setShowPasswordNew] = useState(false);
const [showPasswordConfirmNew, setShowPasswordConfirmNew] = useState(false);
const ResetPasswordSchema = Yup.object().shape({});
const methods = useForm<FormValuesProps>({
resolver: yupResolver(ResetPasswordSchema),
// defaultValues: { email: 'demo@minimals.cc' },
});
const {
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = methods;
const onSubmit = async (data: FormValuesProps) => {
try {
await axios.post('/forget-password', { ...data, token });
console.log(data);
// await new Promise((resolve) => setTimeout(resolve, 500));
await new Promise((resolve) => setTimeout(resolve, 500));
if (isMountedRef.current) {
navigate('/auth/login', { replace: true });
}
} catch (error) {
console.log(error.response.data);
if (isMountedRef.current) {
setError('afterSubmit', { ...error, message: error.response.data.message });
}
}
};
return (
<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={3}>
{!!errors.afterSubmit && <Alert severity="error">{errors.afterSubmit.message}</Alert>}
<Typography>Kata Sandi Baru</Typography>
<RHFTextField
name="new_password"
label="Kata Sandi Baru"
type={showPasswordNew ? 'text' : 'password'}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPasswordNew(!showPasswordNew)} edge="end">
<Iconify icon={showPasswordNew ? 'eva:eye-fill' : 'eva:eye-off-fill'} />
</IconButton>
</InputAdornment>
),
}}
/>
<Typography>Konfirmasi Kata Sandi </Typography>
<RHFTextField
name="confirm_new_password"
label="Konfirmasi Kata Sandi"
type={showPasswordConfirmNew ? 'text' : 'password'}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowPasswordConfirmNew(!showPasswordConfirmNew)}
edge="end"
>
<Iconify icon={showPasswordConfirmNew ? 'eva:eye-fill' : 'eva:eye-off-fill'} />
</IconButton>
</InputAdornment>
),
}}
/>
<LoadingButton
fullWidth
size="large"
type="submit"
variant="contained"
loading={isSubmitting}
>
Reset Password
</LoadingButton>
</Stack>
</FormProvider>
);
}

View File

@@ -0,0 +1 @@
export { default as ForgetPasswordForm } from './ForgetPasswordForm';

View File

@@ -32,7 +32,7 @@ export default function ResetPasswordForm({ onSent, onGetEmail }: Props) {
const methods = useForm<FormValuesProps>({
resolver: yupResolver(ResetPasswordSchema),
defaultValues: { email: 'demo@minimals.cc' },
// defaultValues: { email: 'demo@minimals.cc' },
});
const {

View File

@@ -0,0 +1,19 @@
<table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<a href="{{ $url }}" class="button button-{{ $color ?? 'primary' }}" target="_blank" rel="noopener">{{ $slot }}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,11 @@
<tr>
<td>
<table class="footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>

View File

@@ -0,0 +1,10 @@
<tr>
<td class="header">
<a href="{{ $url }}" style="display: inline-block;">
@if (trim($slot) === 'Laravel')
@else
{{ $slot }}
@endif
</a>
</td>
</tr>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<style>
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
</head>
<body>
<table class="wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
{{ $header ?? '' }}
<!-- Email Body -->
<tr>
<td class="body" width="100%" cellpadding="0" cellspacing="0">
<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
{{ Illuminate\Mail\Markdown::parse($slot) }}
{{ $subcopy ?? '' }}
</td>
</tr>
</table>
</td>
</tr>
{{ $footer ?? '' }}
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,27 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
@endcomponent
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} PrimayaApp. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent

View File

@@ -0,0 +1,14 @@
<table class="panel" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-item">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,7 @@
<table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

View File

@@ -0,0 +1,3 @@
<div class="table">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</div>

View File

@@ -0,0 +1,302 @@
/* Base */
@font-face {
font-family: 'EuclidCircularBBold';
src: url('../../../../../../public/fonts/bold/EuclidCircularBBold.eot');
src: url('../../../../../../public/fonts/bold/EuclidCircularBBold.eot') format('embedded-opentype'),
url('../../../../../../public/fonts/bold/EuclidCircularBBold.woff2') format('woff2'),
url('../../../../../../public/fonts/bold/EuclidCircularBBold.woff') format('woff'),
url('../../../../../../public/fonts/bold/EuclidCircularBBold.ttf') format('truetype'),
url('../../../../../../public/fonts/bold/EuclidCircularBBold.svg#EuclidCircularBBold') format('svg');
}
body,
body *:not(html):not(style):not(br):not(tr):not(code) {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative;
}
body {
-webkit-text-size-adjust: none;
background-color: #ffffff;
color: #718096;
height: 100%;
line-height: 1.4;
margin: 0;
padding: 0;
width: 100% !important;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: #3869d4;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #3d4852;
font-size: 18px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
font-family: 'EuclidCircularBBold';
color: #3869d4;
}
h3 {
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
p.sub {
font-size: 12px;
}
/* img {
max-width: 100%;
} */
/* Layout */
.wrapper {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: #edf2f7;
margin: 0;
padding: 0;
width: 100%;
}
.content {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 0;
padding: 0;
width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a {
color: #3d4852;
font-size: 19px;
font-weight: bold;
text-decoration: none;
}
/* Logo */
.logo {
height: 75px;
max-height: 75px;
width: 75px;
}
/* Body */
.body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: #edf2f7;
border-bottom: 1px solid #edf2f7;
border-top: 1px solid #edf2f7;
margin: 0;
padding: 0;
width: 100%;
}
.inner-body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
background-color: #ffffff;
border-color: #e8e5ef;
border-radius: 2px;
border-width: 1px;
box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015);
margin: 0 auto;
padding: 0;
width: 570px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid #e8e5ef;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 14px;
}
/* Footer */
.footer {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
}
.footer p {
color: #b0adc5;
font-size: 12px;
text-align: center;
}
.footer a {
color: #b0adc5;
text-decoration: underline;
}
/* Tables */
.table table {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 30px auto;
width: 100%;
}
.table th {
border-bottom: 1px solid #edeff2;
margin: 0;
padding-bottom: 8px;
}
.table td {
color: #74787e;
font-size: 15px;
line-height: 18px;
margin: 0;
padding: 10px 0;
margin-bottom: 50px;
text-align: left;
}
.content-cell {
max-width: 100vw;
padding: 32px;
}
/* Buttons */
.action {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
}
.button {
-webkit-text-size-adjust: none;
border-radius: 4px;
color: #fff;
display: inline-block;
overflow: hidden;
text-decoration: none;
}
.button-blue,
.button-primary {
background-color: #2d3748;
border-bottom: 8px solid #2d3748;
border-left: 18px solid #2d3748;
border-right: 18px solid #2d3748;
border-top: 8px solid #2d3748;
}
.button-green,
.button-success {
background-color: #48bb78;
border-bottom: 8px solid #48bb78;
border-left: 18px solid #48bb78;
border-right: 18px solid #48bb78;
border-top: 8px solid #48bb78;
}
.button-red,
.button-error {
background-color: #e53e3e;
border-bottom: 8px solid #e53e3e;
border-left: 18px solid #e53e3e;
border-right: 18px solid #e53e3e;
border-top: 8px solid #e53e3e;
}
/* Panels */
.panel {
border-left: #2d3748 solid 4px;
margin: 21px 0;
}
.panel-content {
background-color: #edf2f7;
color: #718096;
padding: 16px;
}
.panel-content p {
color: #718096;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Utilities */
.break-all {
word-break: break-all;
}

View File

@@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
[{{ $slot }}]({{ $url }})

View File

@@ -0,0 +1,9 @@
{!! strip_tags($header) !!}
{!! strip_tags($slot) !!}
@isset($subcopy)
{!! strip_tags($subcopy) !!}
@endisset
{!! strip_tags($footer) !!}

View File

@@ -0,0 +1,27 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
@endcomponent
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1,13 @@
@component('mail::message')
# Verify Email
Please click the button below to verify your email address.
@component('mail::button', ['url' => $url])
Verify Email
@endcomponent
Thanks,<br>
{{ config('app.name') }}
@endcomponent