feat: back password reset email (#277)
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL;
|
||||||
@@ -19,7 +19,7 @@ model User {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String?
|
password String?
|
||||||
email String
|
email String? @unique
|
||||||
emailVerified Boolean @default(false)
|
emailVerified Boolean @default(false)
|
||||||
googleID String? @unique
|
googleID String? @unique
|
||||||
isGuest Boolean @default(false)
|
isGuest Boolean @default(false)
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
ParseFilePipeBuilder,
|
ParseFilePipeBuilder,
|
||||||
Response,
|
Response,
|
||||||
Param,
|
|
||||||
Query,
|
Query,
|
||||||
|
Param,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
@@ -50,6 +50,7 @@ import { SettingsService } from 'src/settings/settings.service';
|
|||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { writeFile } from 'fs';
|
import { writeFile } from 'fs';
|
||||||
|
import { PasswordResetDto } from './dto/password_reset.dto ';
|
||||||
|
|
||||||
@ApiTags('auth')
|
@ApiTags('auth')
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@@ -115,11 +116,31 @@ export class AuthController {
|
|||||||
@ApiOperation({description: 'Resend the verification email'})
|
@ApiOperation({description: 'Resend the verification email'})
|
||||||
async reverify(@Request() req: any): Promise<void> {
|
async reverify(@Request() req: any): Promise<void> {
|
||||||
const user = await this.usersService.user({ id: req.user.id });
|
const user = await this.usersService.user({ id: req.user.id });
|
||||||
if (!user) throw new BadRequestException("Invalid user");
|
if (!user) throw new BadRequestException('Invalid user');
|
||||||
await this.authService.sendVerifyMail(user);
|
await this.authService.sendVerifyMail(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HttpCode(200)
|
||||||
|
@Put('password-reset')
|
||||||
|
async password_reset(
|
||||||
|
@Body() resetDto: PasswordResetDto,
|
||||||
|
@Query('token') token: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (await this.authService.changePassword(resetDto.password, token)) return;
|
||||||
|
throw new BadRequestException('Invalid token. Expired or invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(200)
|
||||||
|
@Put('forgot-password')
|
||||||
|
async forgot_password(@Query('email') email: string): Promise<void> {
|
||||||
|
console.log(email);
|
||||||
|
const user = await this.usersService.user({ email: email });
|
||||||
|
if (!user) throw new BadRequestException('Invalid user');
|
||||||
|
await this.authService.sendPasswordResetMail(user);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
|
@ApiBody({ type: LoginDto })
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(LocalAuthGuard)
|
@UseGuards(LocalAuthGuard)
|
||||||
@ApiBody({ type: LoginDto })
|
@ApiBody({ type: LoginDto })
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendVerifyMail(user: User) {
|
async sendVerifyMail(user: User) {
|
||||||
if (process.env.IGNORE_MAILS === "true") return;
|
if (process.env.IGNORE_MAILS === 'true') return;
|
||||||
console.log("Sending verification mail to", user.email);
|
if (user.email == null) return;
|
||||||
|
console.log('Sending verification mail to', user.email);
|
||||||
const token = await this.jwtService.signAsync(
|
const token = await this.jwtService.signAsync(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -52,11 +53,45 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendPasswordResetMail(user: User) {
|
||||||
|
if (process.env.IGNORE_MAILS === 'true') return;
|
||||||
|
if (user.email == null) return;
|
||||||
|
console.log('Sending password reset mail to', user.email);
|
||||||
|
const token = await this.jwtService.signAsync(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{ expiresIn: '10h' },
|
||||||
|
);
|
||||||
|
await this.emailService.sendMail({
|
||||||
|
to: user.email,
|
||||||
|
from: 'chromacase@octohub.app',
|
||||||
|
subject: 'Password reset for Chromacase',
|
||||||
|
html: `To reset your password, please click on this <a href="{${process.env.PUBLIC_URL}/password_reset?token=${token}">link</a>.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePassword(new_password: string, token: string): Promise<boolean> {
|
||||||
|
let verified;
|
||||||
|
try {
|
||||||
|
verified = await this.jwtService.verifyAsync(token);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Password reset token failure', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(verified)
|
||||||
|
await this.userService.updateUser({
|
||||||
|
where: { id: verified.userId },
|
||||||
|
data: { password: new_password },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async verifyMail(userId: number, token: string): Promise<boolean> {
|
async verifyMail(userId: number, token: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.jwtService.verifyAsync(token);
|
await this.jwtService.verifyAsync(token);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.log("Verify mail token failure", e);
|
console.log('Verify mail token failure', e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await this.userService.updateUser({
|
await this.userService.updateUser({
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class PasswordResetDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export class User {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
username: string;
|
username: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
email: string;
|
email: string | null;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { User, Prisma } from '@prisma/client';
|
import { User, Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
@@ -53,7 +54,7 @@ export class UsersService {
|
|||||||
username: `Guest ${randomUUID()}`,
|
username: `Guest ${randomUUID()}`,
|
||||||
isGuest: true,
|
isGuest: true,
|
||||||
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
|
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
|
||||||
email: '',
|
email: null,
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -89,6 +90,7 @@ export class UsersService {
|
|||||||
// We could not find a profile icon locally, using gravatar instead.
|
// We could not find a profile icon locally, using gravatar instead.
|
||||||
const user = await this.user({ id: userId });
|
const user = await this.user({ id: userId });
|
||||||
if (!user) throw new InternalServerErrorException();
|
if (!user) throw new InternalServerErrorException();
|
||||||
|
if (!user.email) throw new NotFoundException();
|
||||||
const hash = createHash('md5')
|
const hash = createHash('md5')
|
||||||
.update(user.email.trim().toLowerCase())
|
.update(user.email.trim().toLowerCase())
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import VerifiedView from './views/VerifiedView';
|
|||||||
import SigninView from './views/SigninView';
|
import SigninView from './views/SigninView';
|
||||||
import SignupView from './views/SignupView';
|
import SignupView from './views/SignupView';
|
||||||
import TabNavigation from './components/V2/TabNavigation';
|
import TabNavigation from './components/V2/TabNavigation';
|
||||||
|
import PasswordResetView from './views/PasswordResetView';
|
||||||
|
import ForgotPasswordView from './views/ForgotPasswordView';
|
||||||
|
|
||||||
// Util function to hide route props in URL
|
// Util function to hide route props in URL
|
||||||
const removeMe = () => '';
|
const removeMe = () => '';
|
||||||
@@ -123,6 +125,16 @@ const publicRoutes = () =>
|
|||||||
options: { title: 'Google signin', headerShown: false },
|
options: { title: 'Google signin', headerShown: false },
|
||||||
link: '/logged/google',
|
link: '/logged/google',
|
||||||
},
|
},
|
||||||
|
PasswordReset: {
|
||||||
|
component: PasswordResetView,
|
||||||
|
options: { title: 'Password reset form', headerShown: false },
|
||||||
|
link: '/password_reset',
|
||||||
|
},
|
||||||
|
ForgotPassword: {
|
||||||
|
component: ForgotPasswordView,
|
||||||
|
options: { title: 'Password reset form', headerShown: false },
|
||||||
|
link: '/forgot_password',
|
||||||
|
},
|
||||||
} as const);
|
} as const);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { translate } from '../../i18n/i18n';
|
||||||
|
import { string } from 'yup';
|
||||||
|
import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast } from 'native-base';
|
||||||
|
|
||||||
|
interface ForgotPasswordFormProps {
|
||||||
|
onSubmit: (email: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchemas = {
|
||||||
|
email: string().email('Invalid email').required('Email is required'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ForgotPasswordForm = ({ onSubmit }: ForgotPasswordFormProps) => {
|
||||||
|
const [formData, setFormData] = React.useState({
|
||||||
|
newEmail: {
|
||||||
|
value: '',
|
||||||
|
error: null as string | null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [submittingForm, setSubmittingForm] = React.useState(false);
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
|
||||||
|
<FormControl isRequired isInvalid={formData.newEmail.error !== null}>
|
||||||
|
<FormControl.Label>{translate('newEmail')}</FormControl.Label>
|
||||||
|
<Input
|
||||||
|
isRequired
|
||||||
|
type="text"
|
||||||
|
placeholder={translate('newEmail')}
|
||||||
|
value={formData.newEmail.value}
|
||||||
|
onChangeText={(t) => {
|
||||||
|
let error: null | string = null;
|
||||||
|
validationSchemas.email
|
||||||
|
.validate(t)
|
||||||
|
.catch((e) => (error = e.message))
|
||||||
|
.finally(() => {
|
||||||
|
setFormData({ ...formData, newEmail: { value: t, error } });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
|
||||||
|
{formData.newEmail.error}
|
||||||
|
</FormControl.ErrorMessage>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
isLoading={submittingForm}
|
||||||
|
isDisabled={formData.newEmail.error !== null}
|
||||||
|
onPress={async () => {
|
||||||
|
setSubmittingForm(true);
|
||||||
|
try {
|
||||||
|
const resp = await onSubmit(formData.newEmail.value);
|
||||||
|
toast.show({ description: resp });
|
||||||
|
} catch (e) {
|
||||||
|
toast.show({ description: e as string });
|
||||||
|
} finally {
|
||||||
|
setSubmittingForm(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('submitBtn')}
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForgotPasswordForm;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { translate } from '../../i18n/i18n';
|
||||||
|
import { string } from 'yup';
|
||||||
|
import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast } from 'native-base';
|
||||||
|
|
||||||
|
interface PasswordResetFormProps {
|
||||||
|
onSubmit: (newPassword: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PasswordResetForm = ({ onSubmit }: PasswordResetFormProps) => {
|
||||||
|
const [formData, setFormData] = React.useState({
|
||||||
|
newPassword: {
|
||||||
|
value: '',
|
||||||
|
error: null as string | null,
|
||||||
|
},
|
||||||
|
confirmNewPassword: {
|
||||||
|
value: '',
|
||||||
|
error: null as string | null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [submittingForm, setSubmittingForm] = React.useState(false);
|
||||||
|
|
||||||
|
const validationSchemas = {
|
||||||
|
password: string()
|
||||||
|
.min(4, translate('passwordTooShort'))
|
||||||
|
.max(100, translate('passwordTooLong'))
|
||||||
|
.required('Password is required'),
|
||||||
|
};
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
|
||||||
|
<FormControl
|
||||||
|
isRequired
|
||||||
|
isInvalid={
|
||||||
|
formData.newPassword.error !== null ||
|
||||||
|
formData.confirmNewPassword.error !== null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl.Label>{translate('newPassword')}</FormControl.Label>
|
||||||
|
<Input
|
||||||
|
isRequired
|
||||||
|
type="password"
|
||||||
|
placeholder={translate('newPassword')}
|
||||||
|
value={formData.newPassword.value}
|
||||||
|
onChangeText={(t) => {
|
||||||
|
let error: null | string = null;
|
||||||
|
validationSchemas.password
|
||||||
|
.validate(t)
|
||||||
|
.catch((e) => (error = e.message))
|
||||||
|
.finally(() => {
|
||||||
|
setFormData({ ...formData, newPassword: { value: t, error } });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
|
||||||
|
{formData.newPassword.error}
|
||||||
|
</FormControl.ErrorMessage>
|
||||||
|
|
||||||
|
<FormControl.Label>{translate('confirmNewPassword')}</FormControl.Label>
|
||||||
|
<Input
|
||||||
|
isRequired
|
||||||
|
type="password"
|
||||||
|
placeholder={translate('confirmNewPassword')}
|
||||||
|
value={formData.confirmNewPassword.value}
|
||||||
|
onChangeText={(t) => {
|
||||||
|
let error: null | string = null;
|
||||||
|
validationSchemas.password
|
||||||
|
.validate(t)
|
||||||
|
.catch((e) => (error = e.message));
|
||||||
|
if (!error && t !== formData.newPassword.value) {
|
||||||
|
error = translate('passwordsDontMatch');
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
confirmNewPassword: { value: t, error },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
|
||||||
|
{formData.confirmNewPassword.error}
|
||||||
|
</FormControl.ErrorMessage>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
isLoading={submittingForm}
|
||||||
|
isDisabled={
|
||||||
|
formData.newPassword.error !== null ||
|
||||||
|
formData.confirmNewPassword.error !== null ||
|
||||||
|
formData.newPassword.value === '' ||
|
||||||
|
formData.confirmNewPassword.value === ''
|
||||||
|
}
|
||||||
|
onPress={async () => {
|
||||||
|
setSubmittingForm(true);
|
||||||
|
try {
|
||||||
|
const resp = await onSubmit(formData.newPassword.value);
|
||||||
|
toast.show({ description: resp });
|
||||||
|
} catch (e) {
|
||||||
|
toast.show({ description: e as string });
|
||||||
|
} finally {
|
||||||
|
setSubmittingForm(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('submitBtn')}
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordResetForm;
|
||||||
+3
-3
@@ -54,7 +54,7 @@
|
|||||||
"native-base": "^3.4.17",
|
"native-base": "^3.4.17",
|
||||||
"opensheetmusicdisplay": "^1.7.5",
|
"opensheetmusicdisplay": "^1.7.5",
|
||||||
"phaser": "^3.60.0",
|
"phaser": "^3.60.0",
|
||||||
"react": "18.1.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.1.0",
|
"react-dom": "18.1.0",
|
||||||
"react-i18next": "^11.18.3",
|
"react-i18next": "^11.18.3",
|
||||||
"react-native": "0.70.5",
|
"react-native": "0.70.5",
|
||||||
@@ -91,8 +91,8 @@
|
|||||||
"@storybook/testing-library": "^0.0.13",
|
"@storybook/testing-library": "^0.0.13",
|
||||||
"@testing-library/react-native": "^11.0.0",
|
"@testing-library/react-native": "^11.0.0",
|
||||||
"@types/node": "^18.11.8",
|
"@types/node": "^18.11.8",
|
||||||
"@types/react": "~18.0.24",
|
"@types/react": "~18.2.0",
|
||||||
"@types/react-native": "~0.70.6",
|
"@types/react-native": "~0.70.5",
|
||||||
"@types/react-navigation": "^3.4.0",
|
"@types/react-navigation": "^3.4.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import API from '../API';
|
||||||
|
import { useNavigation } from '../Navigation';
|
||||||
|
import ForgotPasswordForm from '../components/forms/forgotPasswordForm';
|
||||||
|
|
||||||
|
const ForgotPasswordView = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
async function handleSubmit(email: string) {
|
||||||
|
try {
|
||||||
|
await API.fetch({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
route: `/auth/forgot-password?email=${email}`,
|
||||||
|
method: 'PUT',
|
||||||
|
});
|
||||||
|
navigation.navigate('Home');
|
||||||
|
return 'email sent';
|
||||||
|
} catch {
|
||||||
|
return 'Error with email, please contact support';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ForgotPasswordForm onSubmit={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForgotPasswordView;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import API from '../API';
|
||||||
|
import { useNavigation } from '../Navigation';
|
||||||
|
import { useRoute } from '@react-navigation/native';
|
||||||
|
import PasswordResetForm from '../components/forms/passwordResetForm';
|
||||||
|
|
||||||
|
const PasswordResetView = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const handlePasswordReset = async (password: string) => {
|
||||||
|
try {
|
||||||
|
await API.fetch({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
route: `/auth/password-reset?token=${(route.params as any).token}`,
|
||||||
|
method: 'PUT',
|
||||||
|
body: {
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
navigation.navigate('Home');
|
||||||
|
return 'password succesfully reset';
|
||||||
|
} catch {
|
||||||
|
return 'password reset failed';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PasswordResetForm onSubmit={(password) => handlePasswordReset(password)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordResetView;
|
||||||
@@ -100,7 +100,7 @@ const SigninView = () => {
|
|||||||
}}
|
}}
|
||||||
isRequired
|
isRequired
|
||||||
/>,
|
/>,
|
||||||
<LinkBase key={'signin-link'} onPress={() => console.log('Link clicked!')}>
|
<LinkBase key={'signin-link'} onPress={() => navigation.navigate('ForgotPassword')}>
|
||||||
{translate('forgottenPassword')}
|
{translate('forgottenPassword')}
|
||||||
</LinkBase>,
|
</LinkBase>,
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -178,6 +178,23 @@ const StartPageView = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '90%',
|
||||||
|
marginTop: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link href="/forgot_password">I forgot my password</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Column>
|
</Column>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
+3384
-3662
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user