User Auth Flow

This commit is contained in:
Abhinav Jha 2019-12-16 08:49:10 +00:00
parent fb028eb13e
commit ed2ecadbc4
58 changed files with 2758 additions and 1421 deletions

View File

@ -16,7 +16,7 @@ module.exports = {
},
rules: {
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-explicit-any": 0
"@typescript-eslint/no-explicit-any": 0,
},
settings: {
react: {

View File

@ -19,7 +19,6 @@
"@types/lodash": "^4.14.120",
"@types/moment-timezone": "^0.5.10",
"@types/nanoid": "^2.0.0",
"@types/netlify-identity-widget": "^1.4.1",
"@types/node": "^10.12.18",
"@types/react": "^16.8.2",
"@types/react-dom": "^16.8.0",
@ -27,6 +26,7 @@
"@types/react-redux": "^7.0.1",
"@types/react-router-dom": "^5.1.2",
"@types/styled-components": "^4.1.8",
"@types/tinycolor2": "^1.4.2",
"@uppy/core": "^1.5.1",
"@uppy/file-input": "^1.3.1",
"@uppy/google-drive": "^1.3.2",
@ -50,7 +50,6 @@
"monaco-editor": "^0.15.1",
"monaco-editor-webpack-plugin": "^1.7.0",
"nanoid": "^2.0.4",
"netlify-identity-widget": "^1.5.5",
"node-sass": "^4.11.0",
"normalizr": "^3.3.0",
"popper.js": "^1.15.0",
@ -64,13 +63,13 @@
"react-dom": "^16.7.0",
"react-helmet": "^5.2.1",
"react-monaco-editor": "^0.31.1",
"react-netlify-identity": "^0.1.9",
"react-redux": "^6.0.0",
"react-rnd": "^10.1.1",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-scripts": "^3.1.1",
"react-select": "^3.0.8",
"react-transition-group": "^4.3.0",
"react-simple-tree-menu": "^1.1.9",
"react-tabs": "^3.0.0",
"redux": "^4.0.1",
@ -80,6 +79,7 @@
"shallowequal": "^1.1.0",
"source-map-explorer": "^2.1.1",
"styled-components": "^4.1.3",
"tinycolor2": "^1.4.1",
"ts-loader": "^6.0.4",
"typescript": "^3.6.3"
},

View File

@ -2,7 +2,6 @@
<html lang="en">
<head>
<script type="text/javascript" src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
<script type="text/javascript" src="./page.min.js"></script>
<script type="text/javascript" src="/shims/realms-shim.umd.min.js"></script>
@ -11,6 +10,10 @@
<meta name="theme-color" content="#000000" />
<link href="https://fonts.googleapis.com/css?family=DM+Sans:400,500,700&display=swap" rel="stylesheet" />
<link href="../node_modules/normalize.css/normalize.css" rel="stylesheet" />
<!-- blueprint-icons.css file must be included alongside blueprint.css! -->
<link href="../node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css" rel="stylesheet" />
<link href="../node_modules/@blueprintjs/core/lib/css/blueprint.css" rel="stylesheet" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

View File

@ -112,7 +112,7 @@ class ActionAPI extends API {
static updateAPI(
apiConfig: Partial<RestAction>,
): AxiosPromise<ActionCreateUpdateResponse> {
return API.put(`${ActionAPI.url}/${apiConfig.id}`, null, apiConfig);
return API.put(`${ActionAPI.url}/${apiConfig.id}`, apiConfig);
}
static deleteAction(id: string) {

View File

@ -1,21 +1,23 @@
import _ from "lodash";
import axios from "axios";
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { getAppsmithConfigs } from "configs";
import {
REQUEST_TIMEOUT_MS,
REQUEST_HEADERS,
AUTH_CREDENTIALS,
API_REQUEST_HEADERS,
} from "constants/ApiConstants";
import { ActionApiResponse } from "./ActionAPI";
const { apiUrl } = getAppsmithConfigs();
import { AUTH_LOGIN_URL } from "constants/routes";
const { apiUrl, baseUrl } = getAppsmithConfigs();
const axiosInstance = axios.create({
baseURL: apiUrl,
//TODO(abhinav): Refactor this to make more composable.
export const apiRequestConfig = {
baseURL: baseUrl + apiUrl,
timeout: REQUEST_TIMEOUT_MS,
headers: REQUEST_HEADERS,
headers: API_REQUEST_HEADERS,
withCredentials: true,
auth: AUTH_CREDENTIALS,
});
};
const axiosInstance: AxiosInstance = axios.create();
const executeActionRegex = /actions\/execute/;
axiosInstance.interceptors.request.use((config: any) => {
@ -45,9 +47,13 @@ axiosInstance.interceptors.response.use(
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
// console.log(error.response.data);
// console.log(error.response.status);
// console.log(error.response.headers);
if (error.response.status === 401) {
window.location.href = AUTH_LOGIN_URL;
}
return Promise.reject(error.response.data);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
@ -63,29 +69,51 @@ axiosInstance.interceptors.response.use(
);
class Api {
static get(url: string, queryParams?: any) {
static get(
url: string,
queryParams?: any,
config?: Partial<AxiosRequestConfig>,
) {
return axiosInstance.get(
url + this.convertObjectToQueryParams(queryParams),
_.merge(apiRequestConfig, config),
);
}
static post(url: string, body?: any, queryParams?: any) {
static post(
url: string,
body?: any,
queryParams?: any,
config?: Partial<AxiosRequestConfig>,
) {
return axiosInstance.post(
url + this.convertObjectToQueryParams(queryParams),
body,
_.merge(apiRequestConfig, config),
);
}
static put(url: string, queryParams?: any, body?: any) {
static put(
url: string,
body?: any,
queryParams?: any,
config?: Partial<AxiosRequestConfig>,
) {
return axiosInstance.put(
url + this.convertObjectToQueryParams(queryParams),
body,
_.merge(apiRequestConfig, config),
);
}
static delete(url: string, queryParams?: any) {
static delete(
url: string,
queryParams?: any,
config?: Partial<AxiosRequestConfig>,
) {
return axiosInstance.delete(
url + this.convertObjectToQueryParams(queryParams),
_.merge(apiRequestConfig, config),
);
}

View File

@ -89,7 +89,6 @@ class PageApi extends Api {
savePageRequest.pageId,
savePageRequest.layoutId,
),
undefined,
body,
);
}

View File

@ -0,0 +1,67 @@
import { AxiosPromise } from "axios";
import Api from "./Api";
import { ApiResponse } from "./ApiResponses";
export interface LoginUserRequest {
email: string;
password: string;
}
export interface CreateUserRequest {
email: string;
password: string;
}
export interface CreateUserResponse extends ApiResponse {
email: string;
id: string;
}
export interface ForgotPasswordRequest {
email: string;
}
export interface ResetPasswordRequest {
token: string;
user: {
password: string;
email: string;
};
}
export interface ResetPasswordVerifyTokenRequest {
email: string;
token: string;
}
class UserApi extends Api {
static createURL = "v1/users";
static forgotPasswordURL = "v1/users/forgotPassword";
static verifyResetPasswordTokenURL = "v1/users/verifyPasswordResetToken";
static resetPasswordURL = "v1/users/resetPassword";
static createUser(
request: CreateUserRequest,
): AxiosPromise<CreateUserResponse> {
return Api.post(UserApi.createURL, request);
}
static forgotPassword(
request: ForgotPasswordRequest,
): AxiosPromise<ApiResponse> {
return Api.get(UserApi.forgotPasswordURL, request);
}
static resetPassword(
request: ResetPasswordRequest,
): AxiosPromise<ApiResponse> {
return Api.put(UserApi.resetPasswordURL, request);
}
static verifyResetPasswordToken(
request: ResetPasswordVerifyTokenRequest,
): AxiosPromise<ApiResponse> {
return Api.get(UserApi.verifyResetPasswordTokenURL, request);
}
}
export default UserApi;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -77,6 +77,7 @@ export interface TextInputProps extends IInputGroupProps {
showError?: boolean;
/** Additional classname */
className?: string;
type?: string;
refHandler?: (ref: HTMLInputElement | null) => void;
}

View File

@ -0,0 +1,41 @@
import React from "react";
import styled from "styled-components";
import {
Checkbox as BlueprintCheckbox,
ICheckboxProps,
} from "@blueprintjs/core";
import {
IntentColors,
Intent,
getBorderCSSShorthand,
} from "constants/DefaultTheme";
export type CheckboxProps = ICheckboxProps & {
intent: Intent;
align: "left" | "right";
};
export const StyledCheckbox = styled(BlueprintCheckbox)<CheckboxProps>`
&&&& {
span.bp3-control-indicator {
outline: none;
background: white;
box-shadow: none;
border-radius: ${props => props.theme.radii[1]}px;
border: ${props => getBorderCSSShorthand(props.theme.borders[3])};
height: ${props => props.theme.fontSizes[5]}px;
width: ${props => props.theme.fontSizes[5]}px;
}
input:checked ~ span.bp3-control-indicator {
background: ${props => IntentColors[props.intent]};
box-shadow: none;
outline: none;
}
}
`;
export const Checkbox = (props: CheckboxProps) => {
return <StyledCheckbox {...props} alignIndicator={props.align} />;
};
export default Checkbox;

View File

@ -1,7 +1,7 @@
import React, { ReactNode } from "react";
import styled from "styled-components";
import { ItemRenderer, Select } from "@blueprintjs/select";
import { Button, MenuItem } from "@blueprintjs/core";
import { Button, MenuItem, Intent as BlueprintIntent } from "@blueprintjs/core";
import { DropdownOption } from "widgets/DropdownWidget";
import { ControlIconName, ControlIcons } from "icons/ControlIcons";
import { noop } from "utils/AppsmithUtils";
@ -54,7 +54,7 @@ export const ContextDropdown = (props: ContextDropdownProps) => {
onClick={option.onSelect}
shouldDismissPopover={true}
text={option.label || option.value}
intent={option.intent as Intent}
intent={option.intent as BlueprintIntent}
popoverProps={{
minimal: true,
hoverCloseDelay: 0,

View File

@ -0,0 +1,10 @@
import { Divider } from "@blueprintjs/core";
import styled from "styled-components";
export const StyledDivider = styled(Divider)`
&& {
margin: 0;
}
`;
export default StyledDivider;

View File

@ -0,0 +1,8 @@
import { Form } from "redux-form";
import styled from "styled-components";
const StyledForm = styled(Form)`
width: 100%;
`;
export default StyledForm;

View File

@ -0,0 +1,28 @@
import { Button } from "@blueprintjs/core";
import styled from "styled-components";
import { Intent, IntentColors } from "constants/DefaultTheme";
import tinycolor from "tinycolor2";
type FormButtonProps = {
intent: Intent;
};
export default styled(Button)<FormButtonProps>`
&&& {
font-weight: ${props => props.theme.fontWeights[2]};
border: none;
flex-grow: 1;
outline: none;
box-shadow: none;
background: ${props => IntentColors[props.intent]};
&:hover {
background: ${props =>
new tinycolor(IntentColors[props.intent]).darken(10).toString()};
}
&:active {
outline: none;
background: ${props =>
new tinycolor(IntentColors[props.intent]).darken(20).toString()};
}
}
`;

View File

@ -0,0 +1,8 @@
import styled from "styled-components";
import { FormGroup } from "@blueprintjs/core";
const StyledFormGroup = styled(FormGroup)`
& {
width: 100%;
}
`;
export default StyledFormGroup;

View File

@ -0,0 +1,3 @@
import { Spinner } from "@blueprintjs/core";
//TODO(abhinav): Style this when the designs are available.
export default Spinner;

View File

@ -0,0 +1,86 @@
import React from "react";
import styled from "styled-components";
import {
Tag,
Intent as BlueprintIntent,
AnchorButton,
Button,
} from "@blueprintjs/core";
import { Intent, BlueprintIntentsCSS } from "constants/DefaultTheme";
export type MessageAction = {
url?: string;
onClick?: (e: React.SyntheticEvent) => void;
text: string;
intent: Intent;
};
const StyledTag = styled(Tag)`
&&& {
padding: ${props => props.theme.spaces[8]}px;
font-size: ${props => props.theme.fontSizes[4]}px;
text-align: center;
margin-bottom: ${props => props.theme.spaces[4]}px;
p {
white-space: normal;
margin: 0;
}
}
`;
const ActionsContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
& .appsmith-message-action-button {
border: none;
${BlueprintIntentsCSS}
}
`;
const ActionButton = (props: MessageAction) => {
if (props.url) {
return (
<AnchorButton
className="appsmith-message-action-button"
href={props.url}
text={props.text}
minimal
intent={props.intent as BlueprintIntent}
/>
);
} else if (props.onClick) {
return (
<Button
className="appsmith-message-action-button"
onClick={props.onClick}
text={props.text}
minimal
intent={props.intent as BlueprintIntent}
/>
);
}
return null;
};
export type MessageTagProps = {
intent: Intent;
message: string;
title?: string;
actions?: MessageAction[];
};
export const MessageTag = (props: MessageTagProps) => {
const actions =
props.actions &&
props.actions.map(action => <ActionButton key={action.text} {...action} />);
return (
<StyledTag fill large minimal intent={props.intent as BlueprintIntent}>
{props.title && <h4>{props.title}</h4>}
<p>{props.message}</p>
{actions && <ActionsContainer>{actions}</ActionsContainer>}
</StyledTag>
);
};
export default MessageTag;

View File

@ -0,0 +1,10 @@
import React from "react";
import { Field, BaseFieldProps } from "redux-form";
import Checkbox, {
CheckboxProps,
} from "components/designSystems/blueprint/Checkbox";
export const CheckboxField = (props: BaseFieldProps & CheckboxProps) => {
return <Field type="checkbox" component={Checkbox} {...props} />;
};
export default CheckboxField;

View File

@ -5,9 +5,21 @@ import {
TextInputProps,
} from "components/designSystems/appsmith/TextInputComponent";
class TextField extends React.Component<BaseFieldProps & TextInputProps> {
type FieldProps = {
type?: string;
};
class TextField extends React.Component<
BaseFieldProps & TextInputProps & FieldProps
> {
render() {
return <Field component={BaseTextInput} {...this.props} />;
return (
<Field
type={this.props.type || "text"}
component={BaseTextInput}
{...this.props}
/>
);
}
}

View File

@ -1,5 +1,5 @@
import { SENTRY_STAGE_CONFIG } from "constants/ThirdPartyConstants";
import { STAGE_BASE_API_URL } from "constants/ApiConstants";
import { STAGE_BASE_URL } from "constants/ApiConstants";
import { AppsmithUIConfigs } from "./types";
const devConfig: AppsmithUIConfigs = {
@ -13,7 +13,8 @@ const devConfig: AppsmithUIConfigs = {
segment: {
enabled: false,
},
apiUrl: STAGE_BASE_API_URL,
apiUrl: "/api/",
baseUrl: STAGE_BASE_URL,
};
export default devConfig;

View File

@ -13,7 +13,7 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => {
return devConfig;
default:
console.log(
"Unknow environment set: ",
"Unknown environment set: ",
process.env.REACT_APP_ENVIRONMENT,
);
devConfig.apiUrl = "";

View File

@ -3,7 +3,7 @@ import {
HOTJAR_PROD_HJID,
HOTJAR_PROD_HJSV,
} from "constants/ThirdPartyConstants";
import { PROD_BASE_API_URL } from "constants/ApiConstants";
import { PROD_BASE_URL } from "constants/ApiConstants";
import { AppsmithUIConfigs } from "./types";
export const prodConfig: AppsmithUIConfigs = {
@ -21,7 +21,8 @@ export const prodConfig: AppsmithUIConfigs = {
segment: {
enabled: true,
},
apiUrl: PROD_BASE_API_URL,
apiUrl: "/api/",
baseUrl: PROD_BASE_URL,
};
export default prodConfig;

View File

@ -1,5 +1,5 @@
import { SENTRY_STAGE_CONFIG } from "constants/ThirdPartyConstants";
import { STAGE_BASE_API_URL } from "constants/ApiConstants";
import { STAGE_BASE_URL } from "constants/ApiConstants";
import { AppsmithUIConfigs } from "./types";
const stageConfig: AppsmithUIConfigs = {
@ -13,7 +13,8 @@ const stageConfig: AppsmithUIConfigs = {
segment: {
enabled: false,
},
apiUrl: STAGE_BASE_API_URL,
apiUrl: "/api/",
baseUrl: STAGE_BASE_URL,
};
export default stageConfig;

View File

@ -21,4 +21,5 @@ export type AppsmithUIConfigs = {
enabled: boolean;
};
apiUrl: string;
baseUrl: string;
};

View File

@ -2,19 +2,25 @@ export type ContentType =
| "application/json"
| "application/x-www-form-urlencoded";
export const STAGE_BASE_API_URL = "https://appsmith-test.herokuapp.com/api/";
export const PROD_BASE_API_URL = "https://api.appsmith.com/api/";
export const STAGE_BASE_URL = "https://release-api.appsmith.com";
export const PROD_BASE_URL = "https://api.appsmith.com";
export const REQUEST_TIMEOUT_MS = 10000;
export const REQUEST_HEADERS: APIHeaders = {
export const API_REQUEST_HEADERS: APIHeaders = {
"Content-Type": "application/json",
};
export const AUTH_CREDENTIALS = {
username: "api_user",
password: "8uA@;&mB:cnvN~{#",
export const FORM_REQUEST_HEADERS: APIHeaders = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
};
export const OAuthURL = "/oauth2/authorization";
export const GoogleOAuthURL = `${OAuthURL}/google`;
export const GithubOAuthURL = `${OAuthURL}/github`;
export const LOGIN_SUBMIT_PATH = "/login";
export interface APIException {
error: number;
message: string;
@ -22,6 +28,7 @@ export interface APIException {
export interface APIHeaders {
"Content-Type": ContentType;
Accept?: string;
}
export interface APIRequest {

View File

@ -31,6 +31,7 @@ export const Colors: Record<string, string> = {
OXFORD_BLUE: "#2E3D49",
FRENCH_PASS: "#BBE8FE",
CADET_BLUE: "#A3B3BF",
JAFFA: "#F2994A",
};
export type Color = typeof Colors[keyof typeof Colors];

View File

@ -13,7 +13,37 @@ const {
ThemeProvider,
} = styledComponents as styledComponents.ThemedStyledComponentsModule<Theme>;
export type Intent = "primary" | "danger" | "warning" | "none";
export const IntentColors: Record<string, Color> = {
primary: Colors.GREEN,
success: Colors.PURPLE,
secondary: Colors.GEYSER_LIGHT,
danger: Colors.RED,
none: Colors.GEYSER_LIGHT,
warning: Colors.JAFFA,
};
export type Intent = typeof IntentColors[keyof typeof IntentColors];
export const BlueprintIntentsCSS = css`
&.bp3.minimal.bp3-button {
color: ${IntentColors.none};
}
&.bp3.minimal.bp3-intent-primary {
color: ${IntentColors.primary};
}
&.bp3.minimal.bp3-intent-secondary {
color: ${IntentColors.secondary};
}
&.bp3.minimal.bp3-intent-danger {
color: ${IntentColors.danger};
}
&.bp3.minimal.bp3-intent-warning {
color: ${IntentColors.warning};
}
&.bp3.minimal.bp3-intent-success {
color: ${IntentColors.success};
}
`;
export type ThemeBorder = {
thickness: number;
@ -56,6 +86,14 @@ export type Theme = {
hoverBG: Color;
hoverBGOpacity: number;
};
authCard: {
width: number;
borderRadius: number;
background: Color;
padding: number;
dividerSpacing: number;
shadow: string;
};
shadows: string[];
widgets: {
tableWidget: {
@ -143,6 +181,11 @@ export const theme: Theme = {
style: "solid",
color: Colors.GEYSER_LIGHT,
},
{
thickness: 1,
style: "solid",
color: Colors.FRENCH_PASS,
},
],
sidebarWidth: "300px",
headerHeight: "50px",
@ -166,6 +209,14 @@ export const theme: Theme = {
hoverBG: Colors.BLACK,
hoverBGOpacity: 0.5,
},
authCard: {
width: 612,
borderRadius: 16,
background: Colors.WHITE,
padding: 40,
dividerSpacing: 32,
shadow: "0px 4px 8px rgba(9, 30, 66, 0.25)",
},
shadows: ["0px 2px 4px rgba(67, 70, 74, 0.14)"],
widgets: {
tableWidget: {

View File

@ -88,8 +88,19 @@ export const ReduxActionTypes: { [key: string]: string } = {
UPDATE_API_DRAFT: "UPDATE_API_DRAFT",
DELETE_API_DRAFT: "DELETE_API_DRAFT",
UPDATE_ROUTES_PARAMS: "UPDATE_ROUTES_PARAMS",
PERSIST_USER_SESSION: "PERSIST_USER_SESSION",
LOGIN_USER_INIT: "LOGIN_USER_INIT",
LOGIN_USER_SUCCESS: "LOGIN_USER_SUCCESS",
CREATE_USER_INIT: "CREATE_USER_INIT",
CREATE_USER_SUCCESS: "CREATE_USER_SUCCESS",
RESET_USER_PASSWORD_INIT: "RESET_USER_PASSWORD_INIT",
RESET_USER_PASSWORD_SUCCESS: "RESET_USER_PASSWORD_SUCCESS",
FETCH_PLUGINS_REQUEST: "FETCH_PLUGINS_REQUEST",
FETCH_PLUGINS_SUCCESS: "FETCH_PLUGINS_SUCCESS",
FORGOT_PASSWORD_INIT: "FORGOT_PASSWORD_INIT",
FORGOT_PASSWORD_SUCCESS: "FORGOT_PASSWORD_SUCCESS",
RESET_PASSWORD_VERIFY_TOKEN_SUCCESS: "RESET_PASSWORD_VERIFY_TOKEN_SUCCESS",
RESET_PASSWORD_VERIFY_TOKEN_INIT: "RESET_PASSWORD_VERIFY_TOKEN_INIT",
EXECUTE_PAGE_LOAD_ACTIONS: "EXECUTE_PAGE_LOAD_ACTIONS",
};
@ -125,8 +136,13 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
FETCH_PAGE_LIST_ERROR: "FETCH_PAGE_LIST_ERROR",
FETCH_APPLICATION_LIST_ERROR: "FETCH_APPLICATION_LIST_ERROR",
CREATE_APPLICATION_ERROR: "CREATE_APPLICATION_ERROR",
LOGIN_USER_ERROR: "LOGIN_USER_ERROR",
CREATE_USER_ERROR: "CREATE_USER_ERROR",
RESET_USER_PASSWORD_ERROR: "RESET_USER_PASSWORD_ERROR",
SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD",
FETCH_PLUGINS_ERROR: "FETCH_PLUGINS_ERROR",
FORGOT_PASSWORD_ERROR: "FORGOT_PASSWORD_ERROR",
RESET_PASSWORD_VERIFY_TOKEN_ERROR: "RESET_PASSWORD_VERIFY_TOKEN_ERROR",
};
export const ReduxFormActionTypes: { [key: string]: string } = {

View File

@ -0,0 +1,44 @@
import { GoogleOAuthURL, GithubOAuthURL } from "constants/ApiConstants";
import GithubLogo from "assets/images/Github.png";
import GoogleLogo from "assets/images/Google.png";
import { getAppsmithConfigs } from "configs";
const { baseUrl } = getAppsmithConfigs();
export type SocialLoginButtonProps = {
url: string;
name: string;
logo: string;
};
export const GoogleSocialLoginButtonProps: SocialLoginButtonProps = {
url: baseUrl + GoogleOAuthURL,
name: "Google",
logo: GoogleLogo,
};
export const GithubSocialLoginButtonProps: SocialLoginButtonProps = {
url: baseUrl + GithubOAuthURL,
name: "Github",
logo: GithubLogo,
};
export const SocialLoginButtonPropsList: Record<
string,
SocialLoginButtonProps
> = {
google: GoogleSocialLoginButtonProps,
github: GithubSocialLoginButtonProps,
};
export type SocialLoginType = keyof typeof SocialLoginButtonPropsList;
export const getSocialLoginButtonProps = (
logins: SocialLoginType[],
): SocialLoginButtonProps[] => {
return logins.map(login => {
const socialLoginButtonProps = SocialLoginButtonPropsList[login];
if (!socialLoginButtonProps) {
throw Error("Social login not registered: " + login);
}
return socialLoginButtonProps;
});
};

View File

@ -1,2 +1,6 @@
export const API_EDITOR_FORM_NAME = "ApiEditorForm";
export const CREATE_APPLICATION_FORM_NAME = "CreateApplicationForm";
export const LOGIN_FORM_NAME = "LoginForm";
export const SIGNUP_FORM_NAME = "SignupForm";
export const FORGOT_PASSWORD_FORM_NAME = "ForgotPasswordForm";
export const RESET_PASSWORD_FORM_NAME = "ResetPasswordForm";

View File

@ -7,4 +7,78 @@ export const FIELD_REQUIRED_ERROR = "This field is required";
export const VALID_FUNCTION_NAME_ERROR =
"Action name is not a valid function name";
export const UNIQUE_NAME_ERROR = "Action name must be unique";
export const FORM_VALIDATION_EMPTY_EMAIL = "Please enter an email";
export const FORM_VALIDATION_INVALID_EMAIL =
"Please provide a valid email address";
export const FORM_VALIDATION_EMPTY_PASSWORD = "Please enter the password";
export const FORM_VALIDATION_INVALID_PASSWORD =
"Please provide a password with a minimum of 6 characters";
export const LOGIN_PAGE_SUBTITLE = "Use your organization email";
export const LOGIN_PAGE_TITLE = "Login";
export const LOGIN_PAGE_EMAIL_INPUT_LABEL = "Email";
export const LOGIN_PAGE_PASSWORD_INPUT_LABEL = "Password";
export const LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER = "Email";
export const LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER = "Password";
export const LOGIN_PAGE_INVALID_CREDS_ERROR =
"Oops! It looks like you may have forgotten your password.";
export const LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK = "Reset Password";
export const LOGIN_PAGE_LOGIN_BUTTON_TEXT = "Login";
export const LOGIN_PAGE_FORGOT_PASSWORD_TEXT = "Forgot Password";
export const LOGIN_PAGE_REMEMBER_ME_LABEL = "Remember";
export const LOGIN_PAGE_SIGN_UP_LINK_TEXT = "New to Appsmith? Sign up";
export const SIGNUP_PAGE_TITLE = "Sign Up";
export const SIGNUP_PAGE_SUBTITLE = "Use your organization email";
export const SIGNUP_PAGE_EMAIL_INPUT_LABEL = "Email";
export const SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER = " Email";
export const SIGNUP_PAGE_NAME_INPUT_PLACEHOLDER = "Name";
export const SIGNUP_PAGE_NAME_INPUT_LABEL = "Name";
export const SIGNUP_PAGE_PASSWORD_INPUT_LABEL = "Password";
export const SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER = "Password";
export const SIGNUP_PAGE_LOGIN_LINK_TEXT = "Have an account? Login";
export const SIGNUP_PAGE_NAME_INPUT_SUBTEXT = "How should we call you?";
export const SIGNUP_PAGE_SUBMIT_BUTTON_TEXT = "Sign Up";
export const SIGNUP_PAGE_SUCCESS = "Awesome! You have successfully registered.";
export const SIGNUP_PAGE_SUCCESS_LOGIN_BUTTON_TEXT = "Login";
export const RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL = "New Password";
export const RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER = "New Password";
export const RESET_PASSWORD_LOGIN_LINK_TEXT = "Changed your mind? Login";
export const RESET_PASSWORD_PAGE_TITLE = "Reset Password";
export const RESET_PASSWORD_SUBMIT_BUTTON_TEXT = "Reset";
export const RESET_PASSWORD_PAGE_SUBTITLE =
"Create a new password for your account ";
export const RESET_PASSWORD_RESET_SUCCESS =
"Your password has been reset. Please"; //"Your password has been reset. Please login" (see next entry);
export const RESET_PASSWORD_RESET_SUCCESS_LOGIN_LINK = "login";
export const RESET_PASSWORD_EXPIRED_TOKEN =
"The password reset link has expired. Please try generating a new link";
export const RESET_PASSWORD_INVALID_TOKEN =
"The password reset link is invalid. Please try generating a new link";
export const RESET_PASSWORD_FORGOT_PASSWORD_LINK = "Forgot Password";
export const FORGOT_PASSWORD_PAGE_EMAIL_INPUT_LABEL = "Email";
export const FORGOT_PASSWORD_PAGE_EMAIL_INPUT_PLACEHOLDER = "Email";
export const FORGOT_PASSWORD_PAGE_TITLE = "Reset Password";
export const FORGOT_PASSWORD_PAGE_SUBTITLE =
"We will send a reset link to the email below";
export const FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT = "Reset";
export const FORGOT_PASSWORD_SUCCESS_TEXT =
"A password reset link has been sent to";
export const PRIVACY_POLICY_LINK = "Privacy Policy";
export const TERMS_AND_CONDITIONS_LINK = "Terms and Conditions";
export const ERROR_500 =
"We apologize, Something went wrong. We're working to fix things.";
export const ERROR_401 =
"We are unable to verify your identity. Please login again.";
export const ERROR_403 =
"Permission Denied. Please contact your admin to gain access.";
export const WIDGET_TYPE_VALIDATION_ERROR = "Value does not match type";

View File

@ -1,8 +1,8 @@
import { MenuIcons } from "icons/MenuIcons";
export const BASE_URL = "/";
export const LOGIN_URL = "/login";
export const APPLICATIONS_URL = `/applications`;
export const BUILDER_URL = "/applications/:applicationId/pages/:pageId/edit";
export const USER_AUTH_URL = "/user";
export type BuilderRouteParams = {
applicationId: string;
@ -67,3 +67,8 @@ export const EDITOR_ROUTES = [
exact: false,
},
];
export const FORGOT_PASSWORD_URL = `${USER_AUTH_URL}/forgotPassword`;
export const RESET_PASSWORD_URL = `${USER_AUTH_URL}/resetPassword`;
export const SIGN_UP_URL = `${USER_AUTH_URL}/signup`;
export const AUTH_LOGIN_URL = `${USER_AUTH_URL}/login`;

View File

@ -1,3 +1,7 @@
@import "~normalize.css";
@import "~@blueprintjs/core/lib/css/blueprint.css";
@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";
body {
margin: 0;
padding: 0;

View File

@ -2,9 +2,6 @@ import React, { lazy, Suspense } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import Loader from "pages/common/Loader";
import "normalize.css/normalize.css";
import "@blueprintjs/icons/lib/css/blueprint-icons.css";
import "@blueprintjs/core/lib/css/blueprint.css";
import "./index.css";
import * as serviceWorker from "./serviceWorker";
import { Router, Route, Switch } from "react-router-dom";
@ -24,17 +21,17 @@ import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProductio
import {
BASE_URL,
BUILDER_URL,
LOGIN_URL,
APP_VIEW_URL,
APPLICATIONS_URL,
USER_AUTH_URL,
} from "./constants/routes";
const loadingIndicator = <Loader />;
const App = lazy(() => import("./App"));
const UserAuth = lazy(() => import("./pages/UserAuth"));
const Editor = lazy(() => import("./pages/Editor"));
const Applications = lazy(() => import("./pages/Applications"));
const PageNotFound = lazy(() => import("./pages/common/PageNotFound"));
const LoginPage = lazy(() => import("./pages/common/LoginPage"));
const AppViewer = lazy(() => import("./pages/AppViewer"));
appInitializer();
@ -54,6 +51,7 @@ ReactDOM.render(
<Suspense fallback={loadingIndicator}>
<Switch>
<Route exact path={BASE_URL} component={App} />
<Route path={USER_AUTH_URL} component={UserAuth} />
<ProtectedRoute path={BUILDER_URL} component={Editor} />
<ProtectedRoute path={APP_VIEW_URL} component={AppViewer} />
<ProtectedRoute
@ -61,7 +59,6 @@ ReactDOM.render(
path={APPLICATIONS_URL}
component={Applications}
/>
<Route exact path={LOGIN_URL} component={LoginPage} />
<Route component={PageNotFound} />
</Switch>
</Suspense>

View File

@ -4,9 +4,8 @@ import { CREATE_APPLICATION_FORM_NAME } from "constants/forms";
import {
CreateApplicationFormValues,
createApplicationFormSubmitHandler,
} from "utils/formhelpers";
} from "./helpers";
import TextField from "components/editorComponents/form/fields/TextField";
import { required } from "utils/validation/common";
import { FormGroup } from "@blueprintjs/core";
export const CreateApplicationForm = (
@ -15,12 +14,8 @@ export const CreateApplicationForm = (
const { error, handleSubmit } = props;
return (
<Form onSubmit={handleSubmit(createApplicationFormSubmitHandler)}>
<FormGroup intent={error ? "danger" : "none"} helperText={error}>
<TextField
name="applicationName"
placeholder="Name"
validate={required}
/>
<FormGroup intent={error ? "danger" : "none"}>
<TextField name="applicationName" placeholder="Name" />
</FormGroup>
</Form>
);

View File

@ -0,0 +1,24 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { SubmissionError } from "redux-form";
export type CreateApplicationFormValues = {
applicationName: string;
};
export const createApplicationFormSubmitHandler = (
values: CreateApplicationFormValues,
dispatch: any,
): Promise<any> => {
const { applicationName } = values;
return new Promise((resolve, reject) => {
dispatch({
type: ReduxActionTypes.CREATE_APPLICATION_INIT,
payload: {
resolve,
reject,
applicationName,
},
});
}).catch(error => {
throw new SubmissionError(error);
});
};

View File

@ -0,0 +1,119 @@
import React from "react";
import { connect } from "react-redux";
import { withRouter, RouteComponentProps } from "react-router-dom";
import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form";
import StyledForm from "components/editorComponents/Form";
import {
AuthCardContainer,
AuthCardHeader,
AuthCardBody,
FormActions,
} from "./StyledComponents";
import {
FORGOT_PASSWORD_PAGE_EMAIL_INPUT_LABEL,
FORGOT_PASSWORD_PAGE_EMAIL_INPUT_PLACEHOLDER,
FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT,
FORGOT_PASSWORD_PAGE_SUBTITLE,
FORGOT_PASSWORD_PAGE_TITLE,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_INVALID_EMAIL,
FORGOT_PASSWORD_SUCCESS_TEXT,
} from "constants/messages";
import MessageTag from "components/editorComponents/form/MessageTag";
import { FORGOT_PASSWORD_FORM_NAME } from "constants/forms";
import FormGroup from "components/editorComponents/FormGroup";
import FormButton from "components/editorComponents/FormButton";
import TextField from "components/editorComponents/form/fields/TextField";
import { isEmail, isEmptyString } from "utils/formhelpers";
import {
ForgotPasswordFormValues,
forgotPasswordSubmitHandler,
} from "./helpers";
const validate = (values: ForgotPasswordFormValues) => {
const errors: ForgotPasswordFormValues = {};
if (!values.email || isEmptyString(values.email)) {
errors.email = FORM_VALIDATION_EMPTY_EMAIL;
} else if (!isEmail(values.email)) {
errors.email = FORM_VALIDATION_INVALID_EMAIL;
}
return errors;
};
type ForgotPasswordProps = InjectedFormProps<
ForgotPasswordFormValues,
{ emailValue: string }
> &
RouteComponentProps<{ email: string }> & { emailValue: string };
export const ForgotPassword = (props: ForgotPasswordProps) => {
const {
error,
handleSubmit,
pristine,
submitting,
submitFailed,
submitSucceeded,
} = props;
const queryParams = new URLSearchParams(props.location.search);
const hasEmail = queryParams.get("email");
return (
<AuthCardContainer>
{submitSucceeded && (
<MessageTag
intent="success"
message={`${FORGOT_PASSWORD_SUCCESS_TEXT} ${props.emailValue}`}
/>
)}
{submitFailed && error && <MessageTag intent="danger" message={error} />}
<AuthCardHeader>
<h1>{FORGOT_PASSWORD_PAGE_TITLE}</h1>
<h5>{FORGOT_PASSWORD_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<StyledForm onSubmit={handleSubmit(forgotPasswordSubmitHandler)}>
<FormGroup
intent={error ? "danger" : "none"}
label={FORGOT_PASSWORD_PAGE_EMAIL_INPUT_LABEL}
>
<TextField
name="email"
placeholder={FORGOT_PASSWORD_PAGE_EMAIL_INPUT_PLACEHOLDER}
showError
disabled={submitting}
/>
</FormGroup>
<FormActions>
<FormButton
type="submit"
text={FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT}
intent="primary"
disabled={pristine && !hasEmail}
loading={submitting}
/>
</FormActions>
</StyledForm>
</AuthCardBody>
</AuthCardContainer>
);
};
const selector = formValueSelector(FORGOT_PASSWORD_FORM_NAME);
export default connect((state, props: ForgotPasswordProps) => {
const queryParams = new URLSearchParams(props.location.search);
return {
initialValues: {
email: queryParams.get("email") || "",
},
emailValue: selector(state, "email"),
};
})(
reduxForm<ForgotPasswordFormValues, { emailValue: string }>({
validate,
form: FORGOT_PASSWORD_FORM_NAME,
touchOnBlur: true,
})(withRouter(ForgotPassword)),
);

View File

@ -0,0 +1,162 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { connect } from "react-redux";
import { InjectedFormProps, reduxForm, formValueSelector } from "redux-form";
import { Icon } from "@blueprintjs/core";
import { LOGIN_FORM_NAME } from "constants/forms";
import { getAppsmithConfigs } from "configs";
import { FORGOT_PASSWORD_URL, SIGN_UP_URL } from "constants/routes";
import { LOGIN_SUBMIT_PATH } from "constants/ApiConstants";
import {
LOGIN_PAGE_SUBTITLE,
LOGIN_PAGE_TITLE,
LOGIN_PAGE_EMAIL_INPUT_LABEL,
LOGIN_PAGE_PASSWORD_INPUT_LABEL,
LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER,
LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_EMPTY_PASSWORD,
FORM_VALIDATION_INVALID_EMAIL,
FORM_VALIDATION_INVALID_PASSWORD,
LOGIN_PAGE_LOGIN_BUTTON_TEXT,
LOGIN_PAGE_FORGOT_PASSWORD_TEXT,
LOGIN_PAGE_SIGN_UP_LINK_TEXT,
LOGIN_PAGE_INVALID_CREDS_ERROR,
PRIVACY_POLICY_LINK,
TERMS_AND_CONDITIONS_LINK,
LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK,
} from "constants/messages";
import Divider from "components/editorComponents/Divider";
import MessageTag from "components/editorComponents/form/MessageTag";
import FormGroup from "components/editorComponents/FormGroup";
import TextField from "components/editorComponents/form/fields/TextField";
import FormButton from "components/editorComponents/FormButton";
import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth";
import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers";
import { LoginFormValues } from "./helpers";
import {
AuthCardContainer,
SpacedSubmitForm,
FormActions,
AuthCardHeader,
AuthCardFooter,
AuthCardNavLink,
AuthCardBody,
} from "./StyledComponents";
const validate = (values: LoginFormValues) => {
const errors: LoginFormValues = {};
if (!values.password || isEmptyString(values.password)) {
errors.password = FORM_VALIDATION_EMPTY_PASSWORD;
} else if (!isStrongPassword(values.password)) {
errors.password = FORM_VALIDATION_INVALID_PASSWORD;
}
if (!values.username || isEmptyString(values.username)) {
errors.username = FORM_VALIDATION_EMPTY_EMAIL;
} else if (!isEmail(values.username)) {
errors.username = FORM_VALIDATION_INVALID_EMAIL;
}
return errors;
};
type LoginFormProps = { emailValue: string } & InjectedFormProps<
LoginFormValues,
{ emailValue: string }
>;
export const Login = (props: LoginFormProps) => {
const { error, pristine } = props;
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
let showError = false;
if (queryParams.get("error")) {
showError = true;
}
let forgotPasswordURL = `${FORGOT_PASSWORD_URL}`;
if (props.emailValue && !isEmptyString(props.emailValue)) {
forgotPasswordURL += `?email=${props.emailValue}`;
}
const { baseUrl } = getAppsmithConfigs();
return (
<AuthCardContainer>
{showError && pristine && (
<MessageTag
intent="danger"
message={LOGIN_PAGE_INVALID_CREDS_ERROR}
actions={[
{
url: FORGOT_PASSWORD_URL,
text: LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK,
intent: "success",
},
]}
/>
)}
<AuthCardHeader>
<h1>{LOGIN_PAGE_TITLE}</h1>
<h5>{LOGIN_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<SpacedSubmitForm method="POST" action={baseUrl + LOGIN_SUBMIT_PATH}>
<FormGroup
intent={error ? "danger" : "none"}
label={LOGIN_PAGE_EMAIL_INPUT_LABEL}
>
<TextField
name="username"
type="email"
placeholder={LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER}
showError
/>
</FormGroup>
<FormGroup
intent={error ? "danger" : "none"}
label={LOGIN_PAGE_PASSWORD_INPUT_LABEL}
>
<TextField
type="password"
name="password"
placeholder={LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER}
showError
/>
</FormGroup>
<Link to={forgotPasswordURL}>{LOGIN_PAGE_FORGOT_PASSWORD_TEXT}</Link>
<FormActions>
<FormButton
type="submit"
text={LOGIN_PAGE_LOGIN_BUTTON_TEXT}
intent="primary"
/>
</FormActions>
</SpacedSubmitForm>
<Divider />
<ThirdPartyAuth
logins={[SocialLoginTypes.GOOGLE, SocialLoginTypes.GITHUB]}
/>
</AuthCardBody>
<AuthCardNavLink to={SIGN_UP_URL}>
{LOGIN_PAGE_SIGN_UP_LINK_TEXT}
<Icon icon="arrow-right" intent="primary" />
</AuthCardNavLink>
<AuthCardFooter>
<Link to="#">{PRIVACY_POLICY_LINK}</Link>
<Link to="#">{TERMS_AND_CONDITIONS_LINK}</Link>
</AuthCardFooter>
</AuthCardContainer>
);
};
const selector = formValueSelector(LOGIN_FORM_NAME);
export default connect(state => ({
emailValue: selector(state, "email"),
}))(
reduxForm<LoginFormValues, { emailValue: string }>({
validate,
form: LOGIN_FORM_NAME,
})(Login),
);

View File

@ -0,0 +1,224 @@
import React, { useLayoutEffect } from "react";
import { AppState } from "reducers";
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
import { connect } from "react-redux";
import { InjectedFormProps, reduxForm, Field } from "redux-form";
import { RESET_PASSWORD_FORM_NAME } from "constants/forms";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { getIsTokenValid, getIsValidatingToken } from "selectors/authSelectors";
import { Icon } from "@blueprintjs/core";
import TextField from "components/editorComponents/form/fields/TextField";
import MessageTag, {
MessageTagProps,
MessageAction,
} from "components/editorComponents/form/MessageTag";
import Spinner from "components/editorComponents/Spinner";
import FormButton from "components/editorComponents/FormButton";
import FormGroup from "components/editorComponents/FormGroup";
import StyledForm from "components/editorComponents/Form";
import { isEmptyString, isStrongPassword } from "utils/formhelpers";
import { ResetPasswordFormValues, resetPasswordSubmitHandler } from "./helpers";
import {
AuthCardHeader,
AuthCardFooter,
AuthCardContainer,
AuthCardBody,
AuthCardNavLink,
FormActions,
} from "./StyledComponents";
import { AUTH_LOGIN_URL, FORGOT_PASSWORD_URL } from "constants/routes";
import {
RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL,
RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER,
RESET_PASSWORD_LOGIN_LINK_TEXT,
RESET_PASSWORD_SUBMIT_BUTTON_TEXT,
RESET_PASSWORD_PAGE_SUBTITLE,
RESET_PASSWORD_PAGE_TITLE,
FORM_VALIDATION_INVALID_PASSWORD,
FORM_VALIDATION_EMPTY_PASSWORD,
RESET_PASSWORD_EXPIRED_TOKEN,
RESET_PASSWORD_FORGOT_PASSWORD_LINK,
RESET_PASSWORD_INVALID_TOKEN,
RESET_PASSWORD_RESET_SUCCESS,
RESET_PASSWORD_RESET_SUCCESS_LOGIN_LINK,
PRIVACY_POLICY_LINK,
TERMS_AND_CONDITIONS_LINK,
} from "constants/messages";
const validate = (values: ResetPasswordFormValues) => {
const errors: ResetPasswordFormValues = {};
if (!values.password || isEmptyString(values.password)) {
errors.password = FORM_VALIDATION_EMPTY_PASSWORD;
} else if (!isStrongPassword(values.password)) {
errors.password = FORM_VALIDATION_INVALID_PASSWORD;
}
return errors;
};
type ResetPasswordProps = InjectedFormProps<
ResetPasswordFormValues,
{
verifyToken: (token: string, email: string) => void;
isTokenValid: boolean;
validatingToken: boolean;
}
> & {
verifyToken: (token: string, email: string) => void;
isTokenValid: boolean;
validatingToken: boolean;
} & RouteComponentProps<{ email: string; token: string }>;
export const ResetPassword = (props: ResetPasswordProps) => {
const {
error,
handleSubmit,
pristine,
submitting,
submitSucceeded,
submitFailed,
initialValues,
isTokenValid,
validatingToken,
verifyToken,
} = props;
useLayoutEffect(() => {
if (initialValues.token && initialValues.email)
verifyToken(initialValues.token, initialValues.email);
}, [initialValues.token, initialValues.email, verifyToken]);
const showInvalidMessage = !initialValues.token || !initialValues.email;
const showExpiredMessage = !isTokenValid && !validatingToken;
const showSuccessMessage = submitSucceeded && !pristine;
const showFailureMessage = submitFailed && !!error;
let message = "";
let messageActions: MessageAction[] | undefined = undefined;
if (showExpiredMessage || showInvalidMessage) {
messageActions = [
{
url: FORGOT_PASSWORD_URL,
text: RESET_PASSWORD_FORGOT_PASSWORD_LINK,
intent: "success",
},
];
}
if (showExpiredMessage) {
message = RESET_PASSWORD_EXPIRED_TOKEN;
}
if (showInvalidMessage) {
message = RESET_PASSWORD_INVALID_TOKEN;
}
if (showSuccessMessage) {
message = RESET_PASSWORD_RESET_SUCCESS;
messageActions = [
{
url: AUTH_LOGIN_URL,
text: RESET_PASSWORD_RESET_SUCCESS_LOGIN_LINK,
intent: "success",
},
];
}
if (showFailureMessage) {
message = error;
}
const messageTagProps: MessageTagProps = {
intent:
showInvalidMessage || showExpiredMessage || showFailureMessage
? "danger"
: "success",
message,
actions: messageActions,
};
if (showInvalidMessage || showExpiredMessage) {
return <MessageTag {...messageTagProps} />;
}
if (!isTokenValid && validatingToken) {
return <Spinner />;
}
return (
<AuthCardContainer>
{(showSuccessMessage || showFailureMessage) && (
<MessageTag {...messageTagProps} />
)}
<AuthCardHeader>
<h1>{RESET_PASSWORD_PAGE_TITLE}</h1>
<h5>{RESET_PASSWORD_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<StyledForm onSubmit={handleSubmit(resetPasswordSubmitHandler)}>
<FormGroup
intent={error ? "danger" : "none"}
label={RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL}
>
<TextField
name="password"
type="password"
placeholder={RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER}
showError
/>
</FormGroup>
<Field type="hidden" name="email" component="input" />
<Field type="hidden" name="token" component="input" />
<FormActions>
<FormButton
type="submit"
text={RESET_PASSWORD_SUBMIT_BUTTON_TEXT}
intent="primary"
disabled={pristine}
loading={submitting}
/>
</FormActions>
</StyledForm>
</AuthCardBody>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
{RESET_PASSWORD_LOGIN_LINK_TEXT}
<Icon icon="arrow-right" intent="primary" />
</AuthCardNavLink>
<AuthCardFooter>
<Link to="#">{PRIVACY_POLICY_LINK}</Link>
<Link to="#">{TERMS_AND_CONDITIONS_LINK}</Link>
</AuthCardFooter>
</AuthCardContainer>
);
};
export default connect(
(state: AppState, props: ResetPasswordProps) => {
const queryParams = new URLSearchParams(props.location.search);
return {
initialValues: {
email: queryParams.get("email") || undefined,
token: queryParams.get("token") || undefined,
},
isTokenValid: getIsTokenValid(state),
validatingToken: getIsValidatingToken(state),
};
},
(dispatch: any) => ({
verifyToken: (token: string, email: string) =>
dispatch({
type: ReduxActionTypes.RESET_PASSWORD_VERIFY_TOKEN_INIT,
payload: { token, email },
}),
}),
)(
reduxForm<
ResetPasswordFormValues,
{
verifyToken: (token: string, email: string) => void;
validatingToken: boolean;
isTokenValid: boolean;
}
>({
validate,
form: RESET_PASSWORD_FORM_NAME,
touchOnBlur: true,
})(withRouter(ResetPassword)),
);

View File

@ -0,0 +1,145 @@
import React from "react";
import { reduxForm, InjectedFormProps } from "redux-form";
import { AUTH_LOGIN_URL } from "constants/routes";
import { SIGNUP_FORM_NAME } from "constants/forms";
import { Link } from "react-router-dom";
import { Icon } from "@blueprintjs/core";
import Divider from "components/editorComponents/Divider";
import {
AuthCardHeader,
AuthCardBody,
AuthCardFooter,
AuthCardNavLink,
SpacedForm,
FormActions,
AuthCardContainer,
} from "./StyledComponents";
import {
SIGNUP_PAGE_TITLE,
SIGNUP_PAGE_SUBTITLE,
SIGNUP_PAGE_EMAIL_INPUT_LABEL,
SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER,
SIGNUP_PAGE_PASSWORD_INPUT_LABEL,
SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER,
SIGNUP_PAGE_LOGIN_LINK_TEXT,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_EMPTY_PASSWORD,
FORM_VALIDATION_INVALID_EMAIL,
FORM_VALIDATION_INVALID_PASSWORD,
SIGNUP_PAGE_SUBMIT_BUTTON_TEXT,
PRIVACY_POLICY_LINK,
TERMS_AND_CONDITIONS_LINK,
SIGNUP_PAGE_SUCCESS,
SIGNUP_PAGE_SUCCESS_LOGIN_BUTTON_TEXT,
} from "constants/messages";
import MessageTag from "components/editorComponents/form/MessageTag";
import FormGroup from "components/editorComponents/FormGroup";
import TextField from "components/editorComponents/form/fields/TextField";
import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth";
import FormButton from "components/editorComponents/FormButton";
import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers";
import { signupFormSubmitHandler, SignupFormValues } from "./helpers";
const validate = (values: SignupFormValues) => {
const errors: SignupFormValues = {};
if (!values.password || isEmptyString(values.password)) {
errors.password = FORM_VALIDATION_EMPTY_PASSWORD;
} else if (!isStrongPassword(values.password)) {
errors.password = FORM_VALIDATION_INVALID_PASSWORD;
}
if (!values.email || isEmptyString(values.email)) {
errors.email = FORM_VALIDATION_EMPTY_EMAIL;
} else if (!isEmail(values.email)) {
errors.email = FORM_VALIDATION_INVALID_EMAIL;
}
return errors;
};
export const SignUp = (props: InjectedFormProps<SignupFormValues>) => {
const {
error,
handleSubmit,
submitting,
submitFailed,
submitSucceeded,
pristine,
} = props;
return (
<AuthCardContainer>
{submitSucceeded && (
<MessageTag
intent="success"
message={SIGNUP_PAGE_SUCCESS}
actions={[
{
url: AUTH_LOGIN_URL,
text: SIGNUP_PAGE_SUCCESS_LOGIN_BUTTON_TEXT,
intent: "success",
},
]}
/>
)}
{submitFailed && error && <MessageTag intent="danger" message={error} />}
<AuthCardHeader>
<h1>{SIGNUP_PAGE_TITLE}</h1>
<h5>{SIGNUP_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<SpacedForm onSubmit={handleSubmit(signupFormSubmitHandler)}>
<FormGroup
intent={error ? "danger" : "none"}
label={SIGNUP_PAGE_EMAIL_INPUT_LABEL}
>
<TextField
name="email"
type="email"
placeholder={SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER}
showError
/>
</FormGroup>
<FormGroup
intent={error ? "danger" : "none"}
label={SIGNUP_PAGE_PASSWORD_INPUT_LABEL}
>
<TextField
type="password"
name="password"
placeholder={SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER}
showError
/>
</FormGroup>
<FormActions>
<FormButton
type="submit"
disabled={pristine}
loading={submitting}
text={SIGNUP_PAGE_SUBMIT_BUTTON_TEXT}
intent="primary"
/>
</FormActions>
</SpacedForm>
<Divider />
<ThirdPartyAuth
logins={[SocialLoginTypes.GOOGLE, SocialLoginTypes.GITHUB]}
/>
</AuthCardBody>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
{SIGNUP_PAGE_LOGIN_LINK_TEXT}
<Icon icon="arrow-right" intent="primary" />
</AuthCardNavLink>
<AuthCardFooter>
<Link to="#">{PRIVACY_POLICY_LINK}</Link>
<Link to="#">{TERMS_AND_CONDITIONS_LINK}</Link>
</AuthCardFooter>
</AuthCardContainer>
);
};
export default reduxForm<SignupFormValues>({
validate,
form: SIGNUP_FORM_NAME,
touchOnBlur: true,
})(SignUp);

View File

@ -0,0 +1,110 @@
import styled, { css } from "styled-components";
import { Link } from "react-router-dom";
import Form from "components/editorComponents/Form";
import { Card } from "@blueprintjs/core";
export const AuthContainer = styled.section`
width: 100vw;
height: 100vh;
&& .fade {
position: relative;
}
&& .fade-enter {
opacity: 0;
z-index: 1;
}
&& .fade-enter.fade-enter-active {
opacity: 1;
transition: opacity 250ms ease-in;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
display: none;
opacity: 0;
transition: opacity 250ms;
}
`;
export const AuthCard = styled(Card)`
width: ${props => props.theme.authCard.width}px;
background: ${props => props.theme.authCard.background};
border-radius: ${props => props.theme.authCard.borderRadius}px;
padding: ${props => props.theme.authCard.padding}px;
box-shadow: ${props => props.theme.authCard.shadow};
position: relative;
border: none;
& h1,
h5 {
padding: 0;
margin: 0;
font-weight: ${props => props.theme.fontWeights[1]};
}
`;
export const AuthCardContainer = styled.div``;
export const AuthCardHeader = styled.header`
& {
h1 {
font-size: ${props => props.theme.fontSizes[6]}px;
}
h5 {
font-size: ${props => props.theme.fontSizes[4]}px;
}
margin-bottom: ${props => props.theme.authCard.dividerSpacing}px;
}
`;
export const AuthCardNavLink = styled(Link)`
text-align: center;
margin: 0 auto;
display: block;
margin-top: ${props => props.theme.spaces[6]}px;
& span {
margin-left: ${props => props.theme.spaces[4]}px;
}
`;
export const AuthCardFooter = styled.footer`
display: flex;
width: 100%;
justify-content: space-evenly;
align-items: baseline;
margin-top: ${props => props.theme.authCard.dividerSpacing}px;
`;
export const AuthCardBody = styled.div`
display: flex;
justify-content: flex-start;
align-items: stretch;
& a {
margin-top: ${props => props.theme.spaces[8]}px;
font-size: ${props => props.theme.fontSizes[2]}px;
}
`;
const formSpacing = css`
flex-grow: 1;
margin-right: ${props => props.theme.authCard.dividerSpacing}px;
`;
export const SpacedForm = styled(Form)`
${formSpacing}
`;
export const SpacedSubmitForm = styled.form`
${formSpacing}
`;
export const FormActions = styled.div`
display: flex;
justify-content: space-between;
align-items: baseline;
margin-top: ${props => props.theme.spaces[2]}px;
& > label {
margin-right: ${props => props.theme.spaces[11]}px;
}
`;

View File

@ -0,0 +1,87 @@
import React from "react";
import styled from "styled-components";
import {
getSocialLoginButtonProps,
SocialLoginType,
} from "constants/SocialLogin";
import { IntentColors, getBorderCSSShorthand } from "constants/DefaultTheme";
const ThirdPartyAuthWrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
margin-left: ${props => props.theme.authCard.dividerSpacing}px;
`;
//TODO(abhinav): Port this to use themes.
const StyledSocialLoginButton = styled.a`
width: 200px;
display: flex;
align-items: center;
border: ${props => getBorderCSSShorthand(props.theme.borders[2])};
padding: 8px;
color: ${props => props.theme.colors.textDefault};
border-radius: ${props => props.theme.radii[1]}px;
position: relative;
height: 42px;
&:hover {
text-decoration: none;
background: ${IntentColors.success};
color: ${props => props.theme.colors.textOnDarkBG};
}
& > div {
width: 36px;
height: 36px;
padding: ${props => props.theme.radii[1]}px;
position: absolute;
left: 2px;
top: 2px;
background: white;
display: flex;
justify-content: center;
align-items: center;
& img {
width: 80%;
height: 80%;
}
}
& p {
display: block;
margin: 0 0 0 36px;
font-size: ${props => props.theme.fontSizes[3]}px;
font-weight: ${props => props.theme.fontWeights[3]};
}
`;
export const SocialLoginTypes: Record<string, string> = {
GOOGLE: "google",
GITHUB: "github",
};
const SocialLoginButton = (props: {
logo: string;
name: string;
url: string;
}) => {
return (
<StyledSocialLoginButton href={props.url}>
<div>
<img alt={` ${props.name} login`} src={props.logo} />
</div>
<p>{`Sign in with ${props.name}`}</p>
</StyledSocialLoginButton>
);
};
export const ThirdPartyAuth = (props: { logins: SocialLoginType[] }) => {
const socialLoginButtons = getSocialLoginButtonProps(props.logins).map(
item => {
return <SocialLoginButton key={item.name} {...item}></SocialLoginButton>;
},
);
return <ThirdPartyAuthWrapper>{socialLoginButtons}</ThirdPartyAuthWrapper>;
};
export default ThirdPartyAuth;

View File

@ -0,0 +1,85 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { SubmissionError } from "redux-form";
export type LoginFormValues = {
username?: string;
password?: string;
remember?: string;
};
export type SignupFormValues = {
email?: string;
password?: string;
name?: string;
};
export type ResetPasswordFormValues = {
password?: string;
token?: string;
email?: string;
};
export type ForgotPasswordFormValues = {
email?: string;
};
export const signupFormSubmitHandler = (
values: SignupFormValues,
dispatch: any,
): Promise<any> => {
const { email, password } = values;
return new Promise((resolve, reject) => {
dispatch({
type: ReduxActionTypes.CREATE_USER_INIT,
payload: {
resolve,
reject,
email,
password,
},
});
}).catch(error => {
throw new SubmissionError(error);
});
};
export const resetPasswordSubmitHandler = (
values: ResetPasswordFormValues,
dispatch: any,
): Promise<any> => {
const { token, email, password } = values;
return new Promise((resolve, reject) => {
dispatch({
type: ReduxActionTypes.RESET_USER_PASSWORD_INIT,
payload: {
resolve,
reject,
token,
email,
password,
},
});
}).catch(error => {
throw new SubmissionError(error);
});
};
export const forgotPasswordSubmitHandler = (
values: ForgotPasswordFormValues,
dispatch: any,
): Promise<any> => {
const { email } = values;
return new Promise((resolve, reject) => {
dispatch({
type: ReduxActionTypes.FORGOT_PASSWORD_INIT,
payload: {
resolve,
reject,
email,
},
});
}).catch(error => {
error.email = "";
throw new SubmissionError(error);
});
};

View File

@ -0,0 +1,43 @@
import React from "react";
import { Switch, Route, useRouteMatch, useLocation } from "react-router-dom";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import Login from "./Login";
import Centered from "components/designSystems/appsmith/CenteredWrapper";
import { AuthContainer, AuthCard } from "./StyledComponents";
import SignUp from "./SignUp";
import ForgotPassword from "./ForgotPassword";
import ResetPassword from "./ResetPassword";
export const UserAuth = () => {
const { path } = useRouteMatch();
const location = useLocation();
return (
<AuthContainer>
<Centered>
<AuthCard>
<TransitionGroup>
<CSSTransition key={location.key} classNames="fade" timeout={300}>
<Switch location={location}>
<Route exact path={`${path}/login`} component={Login} />
<Route exact path={`${path}/signup`} component={SignUp} />
<Route
exact
path={`${path}/resetPassword`}
component={ResetPassword}
/>
<Route
exact
path={`${path}/forgotPassword`}
component={ForgotPassword}
/>
</Switch>
</CSSTransition>
</TransitionGroup>
</AuthCard>
</Centered>
</AuthContainer>
);
};
export default UserAuth;

View File

@ -1,15 +0,0 @@
import * as React from "react";
import { RouterProps } from "react-router";
import netlifyIdentity from "netlify-identity-widget";
class LoginPage extends React.PureComponent<RouterProps> {
componentDidMount() {
netlifyIdentity.open();
}
render() {
return <div style={{ textAlign: "center" }}></div>;
}
}
export default LoginPage;

View File

@ -1,8 +1,5 @@
import * as React from "react";
import _ from "lodash";
import { Route, Redirect } from "react-router-dom";
import netlifyIdentity from "netlify-identity-widget";
import { Route } from "react-router-dom";
const ProtectedRoute = ({
component: Component,
@ -12,17 +9,7 @@ const ProtectedRoute = ({
component: React.ReactType;
exact?: boolean;
}) => {
const shouldShowLogin =
!_.isNil(netlifyIdentity.currentUser()) ||
process.env.REACT_APP_TESTING === "TESTING";
return (
<Route
{...rest}
render={props =>
shouldShowLogin ? <Component {...props} /> : <Redirect to={"/login"} />
}
/>
);
return <Route {...rest} render={props => <Component {...props} />} />;
};
export default ProtectedRoute;

View File

@ -20,6 +20,7 @@ import { PageListReduxState } from "./entityReducers/pageListReducer";
import { ApiPaneReduxState } from "./uiReducers/apiPaneReducer";
import { RoutesParamsReducerState } from "reducers/uiReducers/routesParamsReducer";
import { PluginDataState } from "reducers/entityReducers/pluginsReducer";
import { AuthState } from "reducers/uiReducers/authReducer";
const appReducer = combineReducers({
entities: entityReducer,
@ -39,6 +40,7 @@ export interface AppState {
applications: ApplicationsReduxState;
apiPane: ApiPaneReduxState;
routesParams: RoutesParamsReducerState;
auth: AuthState;
};
entities: {
canvasWidgets: CanvasWidgetsReduxState;

View File

@ -0,0 +1,32 @@
import { createReducer } from "utils/AppsmithUtils";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
const initialState: AuthState = {
isValidatingToken: true,
isTokenValid: false,
};
const authReducer = createReducer(initialState, {
[ReduxActionTypes.RESET_PASSWORD_VERIFY_TOKEN_INIT]: () => ({
isTokenValid: false,
isValidatingToken: true,
}),
[ReduxActionTypes.RESET_PASSWORD_VERIFY_TOKEN_SUCCESS]: () => ({
isValidatingToken: false,
isTokenValid: true,
}),
[ReduxActionErrorTypes.RESET_PASSWORD_VERIFY_TOKEN_ERROR]: () => ({
isValidatingToken: false,
isTokenValid: false,
}),
});
export interface AuthState {
isValidatingToken: boolean;
isTokenValid: boolean;
}
export default authReducer;

View File

@ -1,15 +1,13 @@
import { createReducer } from "utils/AppsmithUtils";
import {
ReduxAction,
UpdateCanvasPayload,
ReduxActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { WidgetProps } from "widgets/BaseWidget";
import { ContainerWidgetProps } from "widgets/ContainerWidget";
import moment from "moment";
import {
ReduxAction,
UpdateCanvasPayload,
} from "constants/ReduxActionConstants";
const initialState: EditorReduxState = {
loadingStates: {

View File

@ -7,6 +7,7 @@ import applicationsReducer from "./applicationsReducer";
import { widgetSidebarReducer } from "./widgetSidebarReducer";
import apiPaneReducer from "./apiPaneReducer";
import routesParamsReducer from "reducers/uiReducers/routesParamsReducer";
import authReducer from "./authReducer";
const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer,
@ -17,5 +18,6 @@ const uiReducer = combineReducers({
applications: applicationsReducer,
apiPane: apiPaneReducer,
routesParams: routesParamsReducer,
auth: authReducer,
});
export default uiReducer;

View File

@ -8,9 +8,27 @@ import {
import AppToaster from "components/editorComponents/ToastComponent";
import { DEFAULT_ERROR_MESSAGE, DEFAULT_ACTION_ERROR } from "constants/errors";
import { ApiResponse } from "api/ApiResponses";
import { put, takeLatest } from "redux-saga/effects";
import { put, takeLatest, call } from "redux-saga/effects";
import { ERROR_500 } from "constants/messages";
export function* validateResponse(response: ApiResponse) {
export function* callAPI(apiCall: any, requestPayload: any) {
try {
return yield call(apiCall, requestPayload);
} catch (error) {
return yield error;
}
}
const getErrorMessage = (code: number) => {
switch (code) {
case 500:
return ERROR_500;
}
};
export function* validateResponse(response: ApiResponse | any) {
if (!response.responseMeta && response.status) {
throw Error(getErrorMessage(response.status));
}
if (response.responseMeta.success) {
return true;
} else {
@ -24,6 +42,12 @@ export function* validateResponse(response: ApiResponse) {
}
}
export function getResponseErrorMessage(response: ApiResponse) {
return response.responseMeta.error
? response.responseMeta.error.message
: undefined;
}
type ErrorPayloadType = object | { message: string };
let ActionErrorDisplayMap: {
[key: string]: (error: ErrorPayloadType) => string;

View File

@ -13,6 +13,7 @@ import watchActionWidgetMapSagas, {
watchPropertyAndBindingUpdate,
} from "./ActionWidgetMapSagas";
import apiPaneSagas from "./ApiPaneSagas";
import userSagas from "./userSagas";
import pluginSagas from "./PluginSagas";
export function* rootSaga() {
@ -30,6 +31,7 @@ export function* rootSaga() {
spawn(watchActionWidgetMapSagas),
spawn(watchPropertyAndBindingUpdate),
spawn(apiPaneSagas),
spawn(userSagas),
spawn(pluginSagas),
]);
}

View File

@ -0,0 +1,168 @@
import { call, takeLatest, put, all } from "redux-saga/effects";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import UserApi, {
CreateUserRequest,
CreateUserResponse,
ForgotPasswordRequest,
ResetPasswordRequest,
ResetPasswordVerifyTokenRequest,
} from "api/UserApi";
import { ApiResponse } from "api/ApiResponses";
import {
validateResponse,
getResponseErrorMessage,
callAPI,
} from "./ErrorSagas";
export function* createUserSaga(
action: ReduxAction<{
resolve: any;
reject: any;
email: string;
password: string;
}>,
) {
const { email, password, resolve, reject } = action.payload;
try {
const request: CreateUserRequest = { email, password };
const response: CreateUserResponse = yield callAPI(
UserApi.createUser,
request,
);
//TODO(abhinav): DRY this
const isValidResponse = yield validateResponse(response);
if (!isValidResponse) {
const errorMessage = getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
} else {
const { email, name, id } = response.data;
yield put({
type: ReduxActionTypes.CREATE_USER_SUCCESS,
payload: {
email,
name,
id,
},
});
yield call(resolve);
}
} catch (error) {
yield call(reject, { _error: error.message });
yield put({
type: ReduxActionErrorTypes.CREATE_USER_ERROR,
payload: {
error,
},
});
}
}
export function* forgotPasswordSaga(
action: ReduxAction<{ resolve: any; reject: any; email: string }>,
) {
const { email, resolve, reject } = action.payload;
try {
const request: ForgotPasswordRequest = { email };
const response: ApiResponse = yield callAPI(
UserApi.forgotPassword,
request,
);
const isValidResponse = yield validateResponse(response);
if (!isValidResponse) {
const errorMessage = yield getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
} else {
yield put({
type: ReduxActionTypes.FORGOT_PASSWORD_SUCCESS,
});
yield call(resolve);
}
} catch (error) {
console.log(error);
yield call(reject, { _error: error.message });
yield put({
type: ReduxActionErrorTypes.FORGOT_PASSWORD_ERROR,
});
}
}
export function* resetPasswordSaga(
action: ReduxAction<{
resolve: any;
reject: any;
email: string;
token: string;
password: string;
}>,
) {
const { email, token, password, resolve, reject } = action.payload;
try {
const request: ResetPasswordRequest = {
user: {
email,
password,
},
token,
};
const response: ApiResponse = yield callAPI(UserApi.resetPassword, request);
const isValidResponse = yield validateResponse(response);
if (!isValidResponse) {
const errorMessage = yield getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
} else {
yield put({
type: ReduxActionTypes.RESET_USER_PASSWORD_SUCCESS,
});
yield call(resolve);
}
} catch (error) {
console.log(error);
yield call(reject, { _error: error.message });
yield put({
type: ReduxActionErrorTypes.RESET_USER_PASSWORD_ERROR,
payload: {
error: error.message,
},
});
}
}
export function* verifyResetPasswordTokenSaga(
action: ReduxAction<{ token: string; email: string }>,
) {
try {
const request: ResetPasswordVerifyTokenRequest = action.payload;
const response: ApiResponse = yield callAPI(
UserApi.verifyResetPasswordToken,
request,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.RESET_PASSWORD_VERIFY_TOKEN_SUCCESS,
});
}
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.RESET_PASSWORD_VERIFY_TOKEN_ERROR,
});
}
}
export default function* userSagas() {
yield all([
takeLatest(ReduxActionTypes.CREATE_USER_INIT, createUserSaga),
takeLatest(ReduxActionTypes.FORGOT_PASSWORD_INIT, forgotPasswordSaga),
takeLatest(ReduxActionTypes.RESET_USER_PASSWORD_INIT, resetPasswordSaga),
takeLatest(
ReduxActionTypes.RESET_PASSWORD_VERIFY_TOKEN_INIT,
verifyResetPasswordTokenSaga,
),
]);
}

View File

@ -0,0 +1,5 @@
import { AppState } from "reducers";
export const getIsTokenValid = (state: AppState) => state.ui.auth.isTokenValid;
export const getIsValidatingToken = (state: AppState) =>
state.ui.auth.isValidatingToken;

View File

@ -2,7 +2,6 @@ import { ReduxAction } from "../constants/ReduxActionConstants";
import { getAppsmithConfigs } from "configs";
import * as Sentry from "@sentry/browser";
import AnalyticsUtil from "./AnalyticsUtil";
import netlifyIdentity from "netlify-identity-widget";
import FontFaceObserver from "fontfaceobserver";
import PropertyControlRegistry from "./PropertyControlRegistry";
import WidgetBuilderRegistry from "./WidgetRegistry";
@ -29,7 +28,6 @@ export const appInitializer = () => {
WidgetBuilderRegistry.registerWidgetBuilders();
PropertyControlRegistry.registerPropertyControlBuilders();
ValidationRegistry.registerInternalValidators();
netlifyIdentity.init();
moment.tz.setDefault(moment.tz.guess());
const appsmithConfigs = getAppsmithConfigs();
if (appsmithConfigs.sentry.enabled && appsmithConfigs.sentry.config) {
@ -83,4 +81,6 @@ export const getNextWidgetName = (
return prefix + (lastIndex + 1);
};
export const noop = () => {};
export const noop = () => {
console.log("noop");
};

View File

@ -1,25 +1,19 @@
import { SubmissionError } from "redux-form";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
const PASSWORD_MIN_LENGTH = 6;
export type CreateApplicationFormValues = {
applicationName: string;
export const hashPassword = (password: string) => {
return password;
};
export const createApplicationFormSubmitHandler = (
values: CreateApplicationFormValues,
dispatch: any,
): Promise<any> => {
const { applicationName } = values;
return new Promise((resolve, reject) => {
dispatch({
type: ReduxActionTypes.CREATE_APPLICATION_INIT,
payload: {
resolve,
reject,
applicationName,
},
});
}).catch(error => {
throw new SubmissionError(error);
});
export const isEmptyString = (value: string) => {
return !value || value.trim().length === 0 || typeof value !== "string";
};
export const isStrongPassword = (value: string) => {
return value.trim().length >= PASSWORD_MIN_LENGTH;
};
// TODO (abhinav): Use a regex which adheres to standards RFC5322
export const isEmail = (value: string) => {
const re = /^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+.([A-Za-z]{2,5})$/;
return re.test(value);
};

File diff suppressed because it is too large Load Diff