mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 14:35:45 +01:00
improve authentication
This commit is contained in:
23
scm-ui/src/components/ErrorNotification.js
Normal file
23
scm-ui/src/components/ErrorNotification.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import Notification from "./Notification";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error: Error
|
||||||
|
};
|
||||||
|
|
||||||
|
class ErrorNotification extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { error } = this.props;
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Notification type="danger">
|
||||||
|
<strong>Error:</strong> {error.message}
|
||||||
|
</Notification>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorNotification;
|
||||||
20
scm-ui/src/components/Footer.js
Normal file
20
scm-ui/src/components/Footer.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
me: any
|
||||||
|
};
|
||||||
|
|
||||||
|
class Footer extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container is-centered">
|
||||||
|
<p class="has-text-centered">{this.props.me.username}</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import * as React from "react";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type Props = {
|
|||||||
label?: string,
|
label?: string,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
|
autofocus?: boolean,
|
||||||
onChange: string => void
|
onChange: string => void
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,6 +16,14 @@ class InputField extends React.Component<Props> {
|
|||||||
placeholder: ""
|
placeholder: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
field: ?HTMLInputElement;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.autofocus && this.field) {
|
||||||
|
this.field.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
this.props.onChange(event.target.value);
|
this.props.onChange(event.target.value);
|
||||||
};
|
};
|
||||||
@@ -35,6 +44,9 @@ class InputField extends React.Component<Props> {
|
|||||||
{this.renderLabel()}
|
{this.renderLabel()}
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<input
|
<input
|
||||||
|
ref={input => {
|
||||||
|
this.field = input;
|
||||||
|
}}
|
||||||
className="input"
|
className="input"
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
43
scm-ui/src/components/Loading.js
Normal file
43
scm-ui/src/components/Loading.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import injectSheet from "react-jss";
|
||||||
|
import Image from "../images/loading.svg";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
wrapper: {
|
||||||
|
position: "relative"
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
width: "128px",
|
||||||
|
height: "128px",
|
||||||
|
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
|
||||||
|
margin: "64px 0 0 -64px"
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: "128px",
|
||||||
|
height: "128px"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: any
|
||||||
|
};
|
||||||
|
|
||||||
|
class Loading extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<div className={classes.loading}>
|
||||||
|
<img className={classes.image} src={Image} alt="Loading ..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectSheet(styles)(Loading);
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Image from "../images/logo.png";
|
import Image from "../images/logo.png";
|
||||||
|
|
||||||
class Logo extends React.PureComponent {
|
type Props = {};
|
||||||
|
|
||||||
|
class Logo extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
return <img src={Image} alt="SCM-Manager logo" />;
|
return <img src={Image} alt="SCM-Manager logo" />;
|
||||||
}
|
}
|
||||||
|
|||||||
37
scm-ui/src/components/Notification.js
Normal file
37
scm-ui/src/components/Notification.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//@flow
|
||||||
|
import * as React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
type NotificationType = "primary" | "info" | "success" | "warning" | "danger";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: NotificationType,
|
||||||
|
onClose?: () => void,
|
||||||
|
children?: React.Node
|
||||||
|
};
|
||||||
|
|
||||||
|
class Notification extends React.Component<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
type: "info"
|
||||||
|
};
|
||||||
|
|
||||||
|
renderCloseButton() {
|
||||||
|
const { onClose } = this.props;
|
||||||
|
if (onClose) {
|
||||||
|
return <button className="delete" onClick={onClose} />;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { type, children, onClose } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classNames("notification", "is-" + type)}>
|
||||||
|
{this.renderCloseButton()}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification;
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PrimaryNavigationLink from "./PrimaryNavigationLink";
|
import PrimaryNavigationLink from "./PrimaryNavigationLink";
|
||||||
|
|
||||||
|
type Props = {};
|
||||||
|
|
||||||
class PrimaryNavigation extends React.Component<Props> {
|
class PrimaryNavigation extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import * as React from "react";
|
||||||
import { Route, Link } from "react-router-dom";
|
import { Route, Link } from "react-router-dom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -5,30 +5,36 @@ import classNames from "classnames";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string,
|
value: string,
|
||||||
|
disabled: boolean,
|
||||||
|
isLoading: boolean,
|
||||||
large?: boolean,
|
large?: boolean,
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class SubmitButton extends React.Component<Props> {
|
class SubmitButton extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { value, large, fullWidth } = this.props;
|
const { value, large, fullWidth, isLoading, disabled } = this.props;
|
||||||
|
|
||||||
const largeClass = large ? "is-large" : "";
|
const largeClass = large ? "is-large" : "";
|
||||||
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
|
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
|
||||||
|
const loadingClass = isLoading ? "is-loading" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<input
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={disabled}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"button",
|
"button",
|
||||||
"is-link",
|
"is-link",
|
||||||
largeClass,
|
largeClass,
|
||||||
fullWidthClass
|
fullWidthClass,
|
||||||
|
loadingClass
|
||||||
)}
|
)}
|
||||||
value={value}
|
>
|
||||||
/>
|
{value}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,45 +1,50 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import Main from "./Main";
|
import Main from "./Main";
|
||||||
import Login from "./Login";
|
import Login from "./Login";
|
||||||
import { getIsAuthenticated } from "../modules/login";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import { ThunkDispatch } from "redux-thunk";
|
import { ThunkDispatch } from "redux-thunk";
|
||||||
|
import { fetchMe } from "../modules/me";
|
||||||
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
import PrimaryNavigation from "../components/PrimaryNavigation";
|
import PrimaryNavigation from "../components/PrimaryNavigation";
|
||||||
|
import Loading from "../components/Loading";
|
||||||
|
import Notification from "../components/Notification";
|
||||||
|
import Footer from "../components/Footer";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
login: boolean,
|
login: boolean,
|
||||||
username: string,
|
me: any,
|
||||||
getAuthState: () => void,
|
fetchMe: () => void,
|
||||||
loading: boolean
|
loading: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class App extends Component<Props> {
|
class App extends Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.getAuthState();
|
this.props.fetchMe();
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { login, loading } = this.props;
|
const { me, loading } = this.props;
|
||||||
|
|
||||||
let content;
|
let content = [];
|
||||||
let navigation;
|
let navigation;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
content = <div>Loading...</div>;
|
content.push(<Loading />);
|
||||||
} else if (!login) {
|
} else if (!me) {
|
||||||
content = <Login />;
|
content.push(<Login />);
|
||||||
} else {
|
} else {
|
||||||
content = <Main />;
|
content.push(<Main />, <Footer me={me} />);
|
||||||
navigation = <PrimaryNavigation />;
|
navigation = <PrimaryNavigation />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Header>{navigation}</Header>
|
<Header>{navigation}</Header>
|
||||||
{content}
|
{content.map(c => {
|
||||||
|
return c;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -47,12 +52,12 @@ class App extends Component<Props> {
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
|
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
|
||||||
return {
|
return {
|
||||||
getAuthState: () => dispatch(getIsAuthenticated())
|
fetchMe: () => dispatch(fetchMe())
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
return state.login || {};
|
return state.me || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import SubmitButton from "../components/SubmitButton";
|
|||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Avatar from "../images/blib.jpg";
|
import Avatar from "../images/blib.jpg";
|
||||||
|
import ErrorNotification from "../components/ErrorNotification";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
spacing: {
|
|
||||||
paddingTop: "5rem"
|
|
||||||
},
|
|
||||||
avatar: {
|
avatar: {
|
||||||
marginTop: "-70px",
|
marginTop: "-70px",
|
||||||
paddingBottom: "20px"
|
paddingBottom: "20px"
|
||||||
@@ -32,6 +30,8 @@ const styles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
loading: boolean,
|
||||||
|
error: Error,
|
||||||
classes: any,
|
classes: any,
|
||||||
login: (username: string, password: string) => void
|
login: (username: string, password: string) => void
|
||||||
};
|
};
|
||||||
@@ -55,16 +55,26 @@ class Login extends React.Component<Props, State> {
|
|||||||
this.setState({ password: value });
|
this.setState({ password: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit(event: Event) {
|
handleSubmit = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (this.isValid()) {
|
||||||
this.props.login(this.state.username, this.state.password);
|
this.props.login(this.state.username, this.state.password);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
return this.state.username && this.state.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInValid() {
|
||||||
|
return !this.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes, loading, error } = this.props;
|
||||||
return (
|
return (
|
||||||
<section className="hero is-fullheight">
|
<section className="hero has-background-light">
|
||||||
<div className={classes.spacing}>
|
<div className="hero-body">
|
||||||
<div className="container has-text-centered">
|
<div className="container has-text-centered">
|
||||||
<div className="column is-4 is-offset-4">
|
<div className="column is-4 is-offset-4">
|
||||||
<h3 className="title">Login</h3>
|
<h3 className="title">Login</h3>
|
||||||
@@ -77,9 +87,11 @@ class Login extends React.Component<Props, State> {
|
|||||||
alt="SCM-Manager"
|
alt="SCM-Manager"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
<form onSubmit={this.handleSubmit.bind(this)}>
|
<ErrorNotification error={error} />
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
<InputField
|
<InputField
|
||||||
placeholder="Your Username"
|
placeholder="Your Username"
|
||||||
|
autofocus={true}
|
||||||
onChange={this.handleUsernameChange}
|
onChange={this.handleUsernameChange}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
@@ -87,7 +99,12 @@ class Login extends React.Component<Props, State> {
|
|||||||
type="password"
|
type="password"
|
||||||
onChange={this.handlePasswordChange}
|
onChange={this.handlePasswordChange}
|
||||||
/>
|
/>
|
||||||
<SubmitButton value="Login" fullWidth={true} />
|
<SubmitButton
|
||||||
|
value="Login"
|
||||||
|
disabled={this.isInValid()}
|
||||||
|
fullWidth={true}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +116,7 @@ class Login extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
return {};
|
return state.login || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
|
|||||||
import repositories from "./repositories/modules/repositories";
|
import repositories from "./repositories/modules/repositories";
|
||||||
import users from "./users/modules/users";
|
import users from "./users/modules/users";
|
||||||
import login from "./modules/login";
|
import login from "./modules/login";
|
||||||
|
import me from "./modules/me";
|
||||||
|
|
||||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||||
|
|
||||||
@@ -18,7 +19,8 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
router: routerReducer,
|
router: routerReducer,
|
||||||
repositories,
|
repositories,
|
||||||
users,
|
users,
|
||||||
login
|
login,
|
||||||
|
me
|
||||||
});
|
});
|
||||||
|
|
||||||
return createStore(
|
return createStore(
|
||||||
|
|||||||
36
scm-ui/src/images/loading.svg
Normal file
36
scm-ui/src/images/loading.svg
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<svg version="1.1" id="L7" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
|
||||||
|
<path fill="#33B2E8" d="M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3
|
||||||
|
c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
dur="2s"
|
||||||
|
from="0 50 50"
|
||||||
|
to="360 50 50"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
<path fill="#33B2E8" d="M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7
|
||||||
|
c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
dur="1s"
|
||||||
|
from="0 50 50"
|
||||||
|
to="-360 50 50"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
<path fill="#33B2E8" d="M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5
|
||||||
|
L82,35.7z">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
dur="2s"
|
||||||
|
from="0 50 50"
|
||||||
|
to="360 50 50"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,97 +1,58 @@
|
|||||||
//@flow
|
//@flow
|
||||||
|
|
||||||
import { apiClient, NOT_AUTHENTICATED_ERROR } from "../apiclient";
|
import { apiClient, NOT_AUTHENTICATED_ERROR } from "../apiclient";
|
||||||
|
import { fetchMe } from "./me";
|
||||||
|
|
||||||
const LOGIN_URL = "/auth/access_token";
|
const LOGIN_URL = "/auth/access_token";
|
||||||
const AUTHENTICATION_INFO_URL = "/me";
|
|
||||||
|
|
||||||
export const LOGIN = "scm/auth/login";
|
|
||||||
export const LOGIN_REQUEST = "scm/auth/login_request";
|
export const LOGIN_REQUEST = "scm/auth/login_request";
|
||||||
export const LOGIN_SUCCESSFUL = "scm/auth/login_successful";
|
export const LOGIN_SUCCESSFUL = "scm/auth/login_successful";
|
||||||
export const LOGIN_FAILED = "scm/auth/login_failed";
|
export const LOGIN_FAILED = "scm/auth/login_failed";
|
||||||
export const GET_IS_AUTHENTICATED_REQUEST = "scm/auth/is_authenticated_request";
|
|
||||||
export const GET_IS_AUTHENTICATED = "scm/auth/get_is_authenticated";
|
|
||||||
export const IS_AUTHENTICATED = "scm/auth/is_authenticated";
|
|
||||||
export const IS_NOT_AUTHENTICATED = "scm/auth/is_not_authenticated";
|
|
||||||
|
|
||||||
export function getIsAuthenticatedRequest() {
|
export function login(username: string, password: string) {
|
||||||
return {
|
const login_data = {
|
||||||
type: GET_IS_AUTHENTICATED_REQUEST
|
cookie: true,
|
||||||
|
grant_type: "password",
|
||||||
|
username,
|
||||||
|
password
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function getIsAuthenticated() {
|
|
||||||
return function(dispatch: any => void) {
|
return function(dispatch: any => void) {
|
||||||
dispatch(getIsAuthenticatedRequest());
|
dispatch(loginRequest());
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(AUTHENTICATION_INFO_URL)
|
.post(LOGIN_URL, login_data)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json();
|
// not the best way or?
|
||||||
|
dispatch(fetchMe());
|
||||||
|
dispatch(loginSuccessful());
|
||||||
})
|
})
|
||||||
.then(data => {
|
.catch(err => {
|
||||||
if (data) {
|
dispatch(loginFailed(err));
|
||||||
dispatch(isAuthenticated(data.username));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error: Error) => {
|
|
||||||
if (error === NOT_AUTHENTICATED_ERROR) {
|
|
||||||
dispatch(isNotAuthenticated());
|
|
||||||
} else {
|
|
||||||
// TODO: Handle errors other than not_authenticated
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAuthenticated(username: string) {
|
|
||||||
return {
|
|
||||||
type: IS_AUTHENTICATED,
|
|
||||||
username
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNotAuthenticated() {
|
|
||||||
return {
|
|
||||||
type: IS_NOT_AUTHENTICATED
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loginRequest() {
|
export function loginRequest() {
|
||||||
return {
|
return {
|
||||||
type: LOGIN_REQUEST
|
type: LOGIN_REQUEST
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function login(username: string, password: string) {
|
|
||||||
var login_data = {
|
|
||||||
cookie: true,
|
|
||||||
grant_type: "password",
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
};
|
|
||||||
return function(dispatch: any => void) {
|
|
||||||
dispatch(loginRequest());
|
|
||||||
return apiClient.post(LOGIN_URL, login_data).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
dispatch(getIsAuthenticated());
|
|
||||||
dispatch(loginSuccessful());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loginSuccessful() {
|
export function loginSuccessful() {
|
||||||
return {
|
return {
|
||||||
type: LOGIN_SUCCESSFUL
|
type: LOGIN_SUCCESSFUL
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function reducer(
|
export function loginFailed(error: Error) {
|
||||||
state: any = { loading: true },
|
return {
|
||||||
action: any = {}
|
type: LOGIN_FAILED,
|
||||||
) {
|
payload: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state: any = {}, action: any = {}) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case LOGIN:
|
case LOGIN_REQUEST:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -112,21 +73,6 @@ export default function reducer(
|
|||||||
login: false,
|
login: false,
|
||||||
error: action.payload
|
error: action.payload
|
||||||
};
|
};
|
||||||
case IS_AUTHENTICATED:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
login: true,
|
|
||||||
loading: false,
|
|
||||||
username: action.username
|
|
||||||
};
|
|
||||||
case IS_NOT_AUTHENTICATED:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
login: false,
|
|
||||||
loading: false,
|
|
||||||
username: null,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
98
scm-ui/src/modules/me.js
Normal file
98
scm-ui/src/modules/me.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
//@flow
|
||||||
|
|
||||||
|
import { apiClient, NOT_AUTHENTICATED_ERROR } from "../apiclient";
|
||||||
|
|
||||||
|
const AUTHENTICATION_INFO_URL = "/me";
|
||||||
|
|
||||||
|
export const ME_AUTHENTICATED_REQUEST = "scm/auth/me_request";
|
||||||
|
export const ME_AUTHENTICATED_SUCCESS = "scm/auth/me_success";
|
||||||
|
export const ME_AUTHENTICATED_FAILURE = "scm/auth/me_failure";
|
||||||
|
export const ME_UNAUTHENTICATED = "scm/auth/me_unauthenticated";
|
||||||
|
|
||||||
|
export function meRequest() {
|
||||||
|
return {
|
||||||
|
type: ME_AUTHENTICATED_REQUEST
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function meSuccess(user: any) {
|
||||||
|
return {
|
||||||
|
type: ME_AUTHENTICATED_SUCCESS,
|
||||||
|
payload: user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function meFailure(error: Error) {
|
||||||
|
return {
|
||||||
|
type: ME_AUTHENTICATED_FAILURE,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function meUnauthenticated() {
|
||||||
|
return {
|
||||||
|
type: ME_UNAUTHENTICATED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchMe() {
|
||||||
|
return function(dispatch: any => void) {
|
||||||
|
dispatch(meRequest());
|
||||||
|
return apiClient
|
||||||
|
.get(AUTHENTICATION_INFO_URL)
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data) {
|
||||||
|
dispatch(meSuccess(data));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
if (error === NOT_AUTHENTICATED_ERROR) {
|
||||||
|
dispatch(meUnauthenticated());
|
||||||
|
} else {
|
||||||
|
dispatch(meFailure(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(
|
||||||
|
state: any = { loading: true },
|
||||||
|
action: any = {}
|
||||||
|
) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ME_AUTHENTICATED_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: true,
|
||||||
|
me: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
case ME_AUTHENTICATED_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
me: action.payload,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
case ME_AUTHENTICATED_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
me: null,
|
||||||
|
error: action.payload
|
||||||
|
};
|
||||||
|
case ME_UNAUTHENTICATED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
me: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user