Skip to content

Commit

Permalink
Security section routing + set first password (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fuzznimp committed Oct 23, 2020
1 parent b97a363 commit cad3849
Show file tree
Hide file tree
Showing 33 changed files with 997 additions and 127 deletions.
4 changes: 2 additions & 2 deletions README.adoc
Expand Up @@ -75,8 +75,8 @@ mkdir certs
Add the local FQDN in /etc/hosts

----
echo "127.0.0.1 sparta.fewlines.local front.sparta.fewlines.local slidedeck.sparta.fewlines.local" | sudo tee -a /etc/hosts
echo "::1 sparta.fewlines.local front.sparta.fewlines.local slidedeck.sparta.fewlines.local" | sudo tee -a /etc/hosts
echo "127.0.0.1 connect-account.local" | sudo tee -a /etc/hosts
echo "::1 connect-account.local" | sudo tee -a /etc/hosts
----

=== Development Process
Expand Down
38 changes: 38 additions & 0 deletions lib/@types/Password.ts
@@ -0,0 +1,38 @@
export type ProviderUserPasswordSet = {
id: string;
name: string;
user: {
id: string;
passwords: {
available: boolean;
};
};
};

export type SetPasswordErrorRules = {
min_digits: {
error: boolean;
minimum: number;
};
min_non_digits: {
error: boolean;
minimum: number;
};
min_total_characters: {
error: boolean;
minimum: number;
};
};

export type SetPasswordError = {
code: string;
locations: [
{
column: number;
line: number;
},
];
message: string;
path: string[];
rules: SetPasswordErrorRules;
};
9 changes: 9 additions & 0 deletions lib/@types/ProviderUser.ts
Expand Up @@ -17,3 +17,12 @@ export type SingleIdentityProviderUser = {
identity: Identity | null;
};
};

export type User = {
id: string;
};

export type CreateOrUpdatePasswordInput = {
cleartextPassword: string;
userId: string;
};
32 changes: 32 additions & 0 deletions lib/commands/createOrUpdatePassword.ts
@@ -0,0 +1,32 @@
import type { FetchResult } from "apollo-link";
import gql from "graphql-tag";

import { CreateOrUpdatePasswordInput, User } from "@lib/@types/ProviderUser";
import { fetchManagement } from "@src/utils/fetchManagement";

export type CreateOrUpdatePassword = Promise<
FetchResult<{
createOrUpdatePassword: User;
}>
>;

const CREATE_OR_UPDATE_PASSWORD_MUTATION = gql`
mutation createOrUpdatePassword($cleartextPassword: String!, $userId: ID!) {
createOrUpdatePassword(
input: { cleartextPassword: $cleartextPassword, userId: $userId }
) {
id
}
}
`;

export async function createOrUpdatePassword(
command: CreateOrUpdatePasswordInput,
): CreateOrUpdatePassword {
const operation = {
query: CREATE_OR_UPDATE_PASSWORD_MUTATION,
variables: command,
};

return fetchManagement(operation) as CreateOrUpdatePassword;
}
33 changes: 33 additions & 0 deletions lib/queries/isUserPasswordSet.ts
@@ -0,0 +1,33 @@
import type { FetchResult } from "apollo-link";
import gql from "graphql-tag";

import type { ProviderUserPasswordSet } from "@lib/@types/Password";
import { fetchManagement } from "@src/utils/fetchManagement";

const IS_USER_PASSWORD_SET_QUERY = gql`
query isUserPasswordSet($userId: String!) {
provider {
id
name
user(filters: { userId: $userId }) {
id
passwords {
available
}
}
}
}
`;

export async function isUserPasswordSet(
userId: string,
): Promise<FetchResult<{ provider: ProviderUserPasswordSet }>> {
const operation = {
query: IS_USER_PASSWORD_SET_QUERY,
variables: { userId },
};

return fetchManagement(operation) as FetchResult<{
provider: ProviderUserPasswordSet;
}>;
}
2 changes: 2 additions & 0 deletions src/clientErrors.ts
Expand Up @@ -2,4 +2,6 @@ export class ErrorSendingValidationCode extends Error {}

export class ErrorVerifyingValidationCode extends Error {}

export class ErrorSettingPassword extends Error {}

export class NoIdentityFound extends Error {}
43 changes: 17 additions & 26 deletions src/components/Layout.tsx
@@ -1,4 +1,3 @@
import Head from "next/head";
import { useRouter } from "next/router";
import React from "react";
import styled from "styled-components";
Expand All @@ -16,36 +15,28 @@ export const Layout: React.FC = ({ children }) => {
const router = useRouter();

return (
<>
<Head>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<title>Connect Account</title>
</Head>
<Main>
{router && router.pathname !== "/" && (
<MobileDisplayOnly>
<Header />
</MobileDisplayOnly>
)}
<Flex>
{router && router.pathname !== "/" && (
<DesktopNavigationBarWrapper>
<DesktopNavigationBar />
</DesktopNavigationBarWrapper>
)}
<ChildrenContainer>{children}</ChildrenContainer>
</Flex>
{router && router.pathname !== "/" && isMobileNavBarOpen && (
<ClickAwayListener onClick={() => setIsMobileNavbarOpen(false)} />
)}
{router && router.pathname !== "/" && (
<Main>
{router && router.pathname !== "/" && (
<MobileDisplayOnly>
<Header />
<MobileNavigationBar
isOpen={isMobileNavBarOpen}
setIsOpen={setIsMobileNavbarOpen}
/>
{isMobileNavBarOpen && (
<ClickAwayListener onClick={() => setIsMobileNavbarOpen(false)} />
)}
</MobileDisplayOnly>
)}
<Flex>
{router && router.pathname !== "/" && (
<DesktopNavigationBarWrapper>
<DesktopNavigationBar />
</DesktopNavigationBarWrapper>
)}
</Main>
</>
<ChildrenContainer>{children}</ChildrenContainer>
</Flex>
</Main>
);
};

Expand Down
38 changes: 38 additions & 0 deletions src/components/business/SetPassword.tsx
@@ -0,0 +1,38 @@
import React from "react";

import type { SetPasswordError } from "@lib/@types/Password";
import type { User } from "@lib/@types/ProviderUser";
import { HttpVerbs } from "@src/@types/HttpVerbs";
import { ErrorSettingPassword } from "@src/clientErrors";
import { fetchJson } from "@src/utils/fetchJson";

interface SetPasswordProps {
children: (props: {
setPassword: (passwordInput: string) => Promise<SetPasswordOutput>;
}) => JSX.Element;
}

export type SetPasswordOutput = {
data: { createOrUpdatePassword: User };
restrictionRulesError?: SetPasswordError;
};

export const SetPassword: React.FC<SetPasswordProps> = ({ children }) => {
async function setPassword(
passwordInput: string,
): Promise<SetPasswordOutput> {
return fetchJson("/api/auth-connect/set-password", HttpVerbs.POST, {
passwordInput,
}).then((response) => {
if (response.status >= 400 && response.status !== 422) {
throw new ErrorSettingPassword();
}

return response.json();
});
}

return children({
setPassword,
});
};
@@ -1,7 +1,7 @@
import React from "react";

import { StoriesContainer } from "../StoriesContainer";
import AccountOverview from "./AccountOverview";
import { AccountOverview } from "./AccountOverview";

export default { title: "pages/Account Overview", component: AccountOverview };

Expand Down
75 changes: 25 additions & 50 deletions src/components/display/fewlines/AccountOverview/AccountOverview.tsx
@@ -1,59 +1,34 @@
import Link from "next/link";
import React from "react";
import styled from "styled-components";

import { LoginsIcon } from "../Icons/LoginsIcon/LoginsIcon";
import { RightChevron } from "../Icons/RightChevron/RightChevron";
import { NeutralLink } from "../NeutralLink/NeutralLink";
import { ShadowBox } from "../ShadowBox/ShadowBox";
import { SecurityIcon } from "../Icons/SecurityIcon/SecurityIcon";
import { SectionListItem } from "../SectionListItem/SectionListItem";

const AccountOverview: React.FC = () => {
export const SECTION_LIST_CONTENT = {
LOGINS: {
text:
"Manage your logins options, including emails, phone numbers and social logins",
icon: <LoginsIcon />,
},
SECURITY: {
text:
"Set or change your password. You can check your connections history here",
icon: <SecurityIcon />,
},
};

export const AccountOverview: React.FC = () => {
return (
<>
<ShadowBox>
<Content>
<Link href="/account/logins">
<NeutralLink>
<Flex>
<LoginsIcon />
<TextBox>
<Span>LOGINS</Span>
<div>
Manage your logins options, including emails, phone numbers
and social logins
</div>
</TextBox>
<RightChevron />
</Flex>
</NeutralLink>
</Link>
</Content>
</ShadowBox>
{Object.entries(SECTION_LIST_CONTENT).map(([sectionName, content]) => {
return (
<SectionListItem
key={sectionName}
sectionName={sectionName}
content={content}
/>
);
})}
</>
);
};

export default AccountOverview;

const Content = styled.div`
cursor: pointer;
`;

const Flex = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: ${({ theme }) => theme.spaces.xs};
`;

export const TextBox = styled.div`
display: flex;
flex-direction: column;
max-width: 50%;
font-size: ${({ theme }) => theme.fontSizes.xxs};
line-height: ${({ theme }) => theme.lineHeights.title};
`;

export const Span = styled.p`
font-size: ${({ theme }) => theme.fontSizes.s};
`;
Expand Up @@ -26,7 +26,9 @@ export const DesktopNavigationBar: React.FC = () => {
</ListItem>
<ListItem>
<LockIcon />
<ListLabel>Security</ListLabel>
<ListLabel onClick={() => router.push("/account/security")}>
Security
</ListLabel>
</ListItem>
<Separator />
<SwitchLanguageItem onClick={() => router.push("/account/locale")}>
Expand Down
26 changes: 13 additions & 13 deletions src/components/display/fewlines/Header/Header.tsx
Expand Up @@ -7,25 +7,25 @@ export const Header: React.FC = () => {
return (
<Flex>
<img width="90" src={fewlinesLogo} aria-label="Fewlines logo" />
<H5>Account</H5>
<p>Account</p>
</Flex>
);
};

const Flex = styled.div`
display: flex;
align-items: center;
margin: ${({ theme }) => theme.spaces.xs};
`;
margin: ${({ theme }) => `${theme.spaces.xs} ${theme.spaces.xs}`};
const H5 = styled.h5`
padding-left: ${({ theme }) => theme.spaces.xxs};
margin-left: ${({ theme }) => theme.spaces.xxs};
border-left: ${({ theme }) =>
`${theme.colors.blacks[0]} ${theme.borders.thin}`};
font-weight: ${({ theme }) => theme.fontWeights.normal};
font-size: ${({ theme }) => theme.fontSizes.s};
display: flex;
align-items: center;
height: ${({ theme }) => theme.spaces.s};
p {
display: flex;
align-items: center;
height: ${({ theme }) => theme.spaces.s};
padding-left: ${({ theme }) => theme.spaces.xxs};
margin-left: ${({ theme }) => theme.spaces.xxs};
border-left: ${({ theme }) =>
`${theme.colors.blacks[0]} ${theme.borders.thin}`};
font-weight: ${({ theme }) => theme.fontWeights.normal};
font-size: ${({ theme }) => theme.fontSizes.s};
}
`;
2 changes: 1 addition & 1 deletion src/components/display/fewlines/Home/Home.stories.tsx
@@ -1,7 +1,7 @@
import React from "react";

import { StoriesContainer } from "../StoriesContainer";
import Home from "./Home";
import { Home } from "./Home";

export default { title: "pages/Home", component: Home };

Expand Down

2 comments on commit cad3849

@vercel
Copy link

@vercel vercel bot commented on cad3849 Oct 23, 2020

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on cad3849 Oct 23, 2020

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

connect-account – ./

connect-account.vercel.app
connect-account.fewlines.vercel.app
connect-account-git-master.fewlines.vercel.app

Please sign in to comment.