Skip to content

Commit

Permalink
Register/Login Page #26 (#63)
Browse files Browse the repository at this point in the history
* feat: Restyle Login Page
* feat: Add Copyright notices
* feat: Reset Password Page
* feat: Account Registration Page
* docs: Update `register` API endpoint docs and add `reset_password` endpoint
  • Loading branch information
amattu2 authored Oct 6, 2023
1 parent 0e2a9fd commit 17a9ae0
Show file tree
Hide file tree
Showing 10 changed files with 614 additions and 50 deletions.
49 changes: 47 additions & 2 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,46 @@ paths:
mail_sent:
type: boolean

/auth/register:
/auth/reset_password/{uuid}/{reset_token}:
post:
operationId: /register
operationId: /auth/reset_password
tags:
- authentication
security: []
summary: Reset Password with Auth Token
description: This is the final stage to the Forgot Password flow. Reset the account password
using the account `uuid` and `token` provided in the email.
parameters:
- $ref: "#/components/parameters/profile_uuid"
- $ref: "#/components/parameters/reset_token"
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required:
- password1
- password2
properties:
password1:
type: string
example: strongPass123
password2:
type: string
example: strongPass123
description: Must match `password1`
submit:
type: string
example: Reset Password
responses:
"302":
description: Successful request


/auth/preregister:
post:
operationId: /preregister
tags:
- authentication
security: []
Expand Down Expand Up @@ -1770,6 +1807,14 @@ components:
schema:
type: string
example: 1GNEK13Z43J126164
reset_token:
name: reset_token
description: The `reset_token` provided by the reset password email
in: path
required: true
schema:
type: string
example: XXXXXXXXXXXXXXXXXXXXX
profile_uuid:
name: uuid
description: The `uuid` of the profile to fetch content for or against
Expand Down
Binary file added src/assets/images/shop-1864x1400.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/config/Endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export const POST_TYPES: FeedPost["type"][] = ["photo", "generic", "list_add"];
export const ENDPOINTS = {
/* Auth Endpoints */
authenticate: `${API_URL}auth/authenticate`,
reset_password: `${API_URL}auth/pwreset`,
logout: `${API_URL}auth/logout`,
register: `${API_URL}auth/preregister`,
// TODO: Password Change Endpoint...

/* Person Endpoints */
Expand Down
4 changes: 4 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Home from './pages';
import Documentation from './pages/documentation';
import Lists from './pages/lists/Controller';
import Login from './pages/login/Controller';
import Register from './pages/register/Controller';
import ForgotPassword from './pages/forgotPassword/Controller';
import Logout from './pages/logout/Controller';
import Post from './pages/post/Controller';
import Profile from './pages/profile/Controller';
Expand Down Expand Up @@ -62,6 +64,8 @@ root.render(
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/logout" element={<Logout />} />
<Route path="/documentation" element={<Documentation />} />
<Route path="*" element={<ProtectedRoutes />} />
Expand Down
6 changes: 6 additions & 0 deletions src/pages/forgotPassword/Controller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from "react";
import ForgotView from "./View";

const ForgotController = () => <ForgotView />;

export default ForgotController;
185 changes: 185 additions & 0 deletions src/pages/forgotPassword/View.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import React, { ElementType, useState } from "react";
import { useForm } from "react-hook-form";
import { Link, LinkProps, useNavigate } from "react-router-dom";
import { Box, Button, Stack, TextField, Typography, styled } from "@mui/material";
import backgroundImage from "../../assets/images/shop-1864x1400.jpg";
import Loader from "../../components/Loader";
import { ENDPOINTS, STATUS_OK } from "../../config/Endpoints";

type FormInput = {
email: string;
};

const StyledContainer = styled(Box)({
height: "100%",
backgroundImage: `linear-gradient(rgba(255, 255, 255, 0.75) 5%, rgba(255, 255, 255, 0.93) 70%), url(${backgroundImage})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
backgroundAttachment: "fixed",
});

const FormContainer = styled(Stack)(({ theme }) => ({
maxWidth: '470px',
margin: '0 auto',
background: "#fff",
borderRadius: "0 0 6px 6px",
padding: "98px 35px",
[theme.breakpoints.down('sm')]: {
height: '100%',
width: '100%',
maxWidth: '100%',
borderRadius: "0",
padding: "25px",
},
}));

const StyledHeaderBox = styled(Box)({
marginBottom: "50px",
textAlign: "center",
});

const StyledHeader = styled(Typography)(({ theme }) => ({
background: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
padding: theme.spacing(1),
borderRadius: "6px",
}));

const StyledSubtitle = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(1),
fontStyle: "italic",
fontWeight: 400,
color: theme.palette.text.secondary,
}));

const StyledFormBox = styled(Box)({
width: "100%",
});

const StyledFooterBox = styled(Box)({
marginTop: "30px",
textAlign: "center",
});

const StyledTextField = styled(TextField)({
marginBottom: "30px",
"& .MuiFormLabel-root": {
fontSize: "18px",
},
});

const StyledButton = styled(Button)(({ theme }) => ({
color: "#fff",
padding: theme.spacing(1.5, 2),
margin: theme.spacing(2, 0),
textTransform: "none",
boxShadow: theme.shadows[4],
}));

const StyledError = styled(Typography)(({ theme }) => ({
fontSize: "12px",
lineHeight: "33px",
color: theme.palette.error.main,
textAlign: "right",
}));

const StyledForgotDetails = styled(Typography)<{ component: ElementType } & LinkProps>(({ theme }) => ({
fontWeight: 500,
color: theme.palette.primary.main,
marginLeft: theme.spacing(0.5),
textDecoration: "none",
}));

const StyledCopyright = styled(Typography)({
position: "absolute",
bottom: "10px",
left: "50%",
transform: "translateX(-50%)",
fontSize: "12px",
textAlign: "center",
});

const ForgotView = () => {
const navigate = useNavigate();

const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");

const { register, handleSubmit, formState, resetField } = useForm<FormInput>();
const { errors } = formState;

const onSubmit = async ({ email }: FormInput) => {
setLoading(true);
setError("");

const response = await fetch(ENDPOINTS.reset_password, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
}).catch(() => null);

const { status, mail_sent, message } = await response?.json() || {};
if (status === STATUS_OK && !!mail_sent) {
navigate("/login", { state: { message: "Password reset email sent" } });
} else {
resetField("email");
setError(message || "Failed to send password reset email");
}

setLoading(false);
};

return (
<StyledContainer>
{loading && <Loader />}
<FormContainer alignItems="center" justifyContent="center">
<StyledHeaderBox>
<StyledHeader variant="h3">
Better VINwiki
</StyledHeader>
<StyledSubtitle variant="subtitle1">
A reimagined VINwiki web experience.
</StyledSubtitle>
</StyledHeaderBox>
<StyledFormBox component="form" onSubmit={handleSubmit(onSubmit)}>
<StyledTextField
{...register("email", { required: true })}
label="Email Address"
type="email"
autoComplete="username"
error={!!errors.email}
helperText={errors.email ? "Email address is required" : ""}
variant="standard"
size="medium"
margin="normal"
fullWidth
autoFocus
/>
{error && (<StyledError>{error}</StyledError>)}
<StyledButton type="submit" fullWidth variant="contained">
Reset Password
</StyledButton>
</StyledFormBox>
<StyledFooterBox>
<Typography component="p" variant="subtitle1">
Already have an account?
<StyledForgotDetails component={Link} to="/login" variant="subtitle1">
Login.
</StyledForgotDetails>
</Typography>
</StyledFooterBox>
</FormContainer>
<StyledCopyright>
&copy;
{" "}
{new Date().getFullYear()}
{" Alec M. "}
<Link to="https://amattu.com" target="_blank" rel="noopener noreferrer">amattu.com</Link>
</StyledCopyright>
</StyledContainer>
);
};

export default ForgotView;
Loading

0 comments on commit 17a9ae0

Please sign in to comment.