mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-17 18:51:10 +01:00
allow unprotected pages
This commit is contained in:
15
scm-ui/.vscode/launch.json
vendored
Normal file
15
scm-ui/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:3000",
|
||||||
|
"webRoot": "${workspaceFolder}/src"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,10 +6,14 @@
|
|||||||
* Flow Language Support
|
* Flow Language Support
|
||||||
* Prettier - Code formatter
|
* Prettier - Code formatter
|
||||||
* Project Snippets
|
* Project Snippets
|
||||||
|
* Debugger for Chrome
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
code --install-extension EditorConfig.EditorConfig
|
code --install-extension EditorConfig.EditorConfig
|
||||||
code --install-extension flowtype.flow-for-vscode
|
code --install-extension flowtype.flow-for-vscode
|
||||||
code --install-extension esbenp.prettier-vscode
|
code --install-extension esbenp.prettier-vscode
|
||||||
code --install-extension rebornix.project-snippets
|
code --install-extension rebornix.project-snippets
|
||||||
|
|
||||||
|
# debugging with chrome browser
|
||||||
|
code --install-extension msjsdiag.debugger-for-chrome
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
// get api base url from environment
|
// get api base url from environment
|
||||||
const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "/scm";
|
const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "/scm";
|
||||||
|
|
||||||
export const PAGE_NOT_FOUND_ERROR = Error("page not found");
|
export const NOT_FOUND_ERROR = Error("not found");
|
||||||
export const NOT_AUTHENTICATED_ERROR = Error("not authenticated");
|
export const UNAUTHORIZED_ERROR = Error("unauthorized");
|
||||||
|
|
||||||
const fetchOptions: RequestOptions = {
|
const fetchOptions: RequestOptions = {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
@@ -16,10 +16,10 @@ const fetchOptions: RequestOptions = {
|
|||||||
function handleStatusCode(response: Response) {
|
function handleStatusCode(response: Response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
throw NOT_AUTHENTICATED_ERROR;
|
throw UNAUTHORIZED_ERROR;
|
||||||
}
|
}
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
throw PAGE_NOT_FOUND_ERROR;
|
throw NOT_FOUND_ERROR;
|
||||||
}
|
}
|
||||||
throw new Error("server returned status code " + response.status);
|
throw new Error("server returned status code " + response.status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import Notification from "./Notification";
|
import Notification from "./Notification";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error: Error
|
error?: Error
|
||||||
};
|
};
|
||||||
|
|
||||||
class ErrorNotification extends React.Component<Props> {
|
class ErrorNotification extends React.Component<Props> {
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ import React from "react";
|
|||||||
import type { Me } from "../types/me";
|
import type { Me } from "../types/me";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
me: Me
|
me?: Me
|
||||||
};
|
};
|
||||||
|
|
||||||
class Footer extends React.Component<Props> {
|
class Footer extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
|
const { me } = this.props;
|
||||||
|
if (!me) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
<div className="container is-centered">
|
<div className="container is-centered">
|
||||||
<p className="has-text-centered">{this.props.me.username}</p>
|
<p className="has-text-centered">{me.username}</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PrimaryNavigationLink from "./PrimaryNavigationLink";
|
import PrimaryNavigationLink from "./PrimaryNavigationLink";
|
||||||
import PrimaryNavigationAction from "./PrimaryNavigationAction";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {};
|
||||||
onLogout: () => void
|
|
||||||
};
|
|
||||||
|
|
||||||
class PrimaryNavigation extends React.Component<Props> {
|
class PrimaryNavigation extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
@@ -13,10 +10,7 @@ class PrimaryNavigation extends React.Component<Props> {
|
|||||||
<nav className="tabs is-boxed">
|
<nav className="tabs is-boxed">
|
||||||
<ul>
|
<ul>
|
||||||
<PrimaryNavigationLink to="/users" label="Users" />
|
<PrimaryNavigationLink to="/users" label="Users" />
|
||||||
<PrimaryNavigationAction
|
<PrimaryNavigationLink to="/logout" label="Logout" />
|
||||||
onClick={this.props.onLogout}
|
|
||||||
label="Logout"
|
|
||||||
/>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
label: string,
|
|
||||||
onClick: () => void
|
|
||||||
};
|
|
||||||
|
|
||||||
class PrimaryNavigationAction extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { label, onClick } = this.props;
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<a onClick={onClick}>{label}</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PrimaryNavigationAction;
|
|
||||||
39
scm-ui/src/components/ProtectedRoute.js
Normal file
39
scm-ui/src/components/ProtectedRoute.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//@flow
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { Route, Redirect, withRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
authenticated?: boolean,
|
||||||
|
component: Component<any, any>
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProtectedRoute extends React.Component<Props> {
|
||||||
|
renderRoute = (Component: any, authenticated?: boolean) => {
|
||||||
|
return (routeProps: any) => {
|
||||||
|
if (authenticated) {
|
||||||
|
return <Component {...routeProps} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Redirect
|
||||||
|
to={{
|
||||||
|
pathname: "/login",
|
||||||
|
state: { from: routeProps.location }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { component, authenticated, ...routeProps } = this.props;
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
{...routeProps}
|
||||||
|
render={this.renderRoute(component, authenticated)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(ProtectedRoute);
|
||||||
@@ -22,7 +22,6 @@ class SubmitButton extends React.Component<Props> {
|
|||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"button",
|
"button",
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import type { ThunkDispatch } from "redux-thunk";
|
|
||||||
import Main from "./Main";
|
import Main from "./Main";
|
||||||
import Login from "./Login";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import { fetchMe } from "../modules/me";
|
import { fetchMe } from "../modules/auth";
|
||||||
import { logout } from "../modules/auth";
|
|
||||||
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import "../components/ConfirmAlert.css";
|
import "../components/ConfirmAlert.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 Loading from "../components/Loading";
|
||||||
import ErrorNotification from "../components/ErrorNotification";
|
|
||||||
import ErrorPage from "../components/ErrorPage";
|
import ErrorPage from "../components/ErrorPage";
|
||||||
|
import Footer from "../components/Footer";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
me: any,
|
me: any,
|
||||||
error: Error,
|
error: Error,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
fetchMe: () => void,
|
authenticated?: boolean,
|
||||||
logout: () => void
|
fetchMe: () => void
|
||||||
};
|
};
|
||||||
|
|
||||||
class App extends Component<Props> {
|
class App extends Component<Props> {
|
||||||
@@ -28,20 +25,15 @@ class App extends Component<Props> {
|
|||||||
this.props.fetchMe();
|
this.props.fetchMe();
|
||||||
}
|
}
|
||||||
|
|
||||||
logout = () => {
|
|
||||||
this.props.logout();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { me, loading, error } = this.props;
|
const { entry, loading, error, authenticated } = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
let navigation;
|
const navigation = authenticated ? <PrimaryNavigation /> : "";
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
content = <Loading />;
|
content = <Loading />;
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
// TODO add error page instead of plain notification
|
|
||||||
content = (
|
content = (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
title="Error"
|
title="Error"
|
||||||
@@ -49,31 +41,31 @@ class App extends Component<Props> {
|
|||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (!me) {
|
|
||||||
content = <Login />;
|
|
||||||
} else {
|
} else {
|
||||||
content = <Main me={me} />;
|
content = <Main authenticated={authenticated} />;
|
||||||
navigation = <PrimaryNavigation onLogout={this.logout} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Header>{navigation}</Header>
|
<Header>{navigation}</Header>
|
||||||
{content}
|
{content}
|
||||||
|
<Footer me={entry} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
return {
|
return {
|
||||||
fetchMe: () => dispatch(fetchMe()),
|
fetchMe: () => dispatch(fetchMe())
|
||||||
logout: () => dispatch(logout())
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
return state.me || {};
|
let mapped = state.auth.me || {};
|
||||||
|
if (state.auth.login) {
|
||||||
|
mapped.authenticated = state.auth.login.authenticated;
|
||||||
|
}
|
||||||
|
return mapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Redirect, withRouter } from "react-router-dom";
|
||||||
import injectSheet from "react-jss";
|
import injectSheet from "react-jss";
|
||||||
import { login } from "../modules/auth";
|
import { login } from "../modules/auth";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -30,9 +31,15 @@ const styles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
loading: boolean,
|
authenticated?: boolean,
|
||||||
error: Error,
|
loading?: boolean,
|
||||||
|
error?: Error,
|
||||||
|
|
||||||
classes: any,
|
classes: any,
|
||||||
|
|
||||||
|
from: any,
|
||||||
|
location: any,
|
||||||
|
|
||||||
login: (username: string, password: string) => void
|
login: (username: string, password: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,8 +77,17 @@ class Login extends React.Component<Props, State> {
|
|||||||
return !this.isValid();
|
return !this.isValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderRedirect = () => {
|
||||||
|
const { from } = this.props.location.state || { from: { pathname: "/" } };
|
||||||
|
return <Redirect to={from} />;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, loading, error } = this.props;
|
const { authenticated, loading, error, classes } = this.props;
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
return this.renderRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="hero has-background-light">
|
<section className="hero has-background-light">
|
||||||
@@ -117,7 +133,7 @@ class Login extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
return state.login || {};
|
return state.auth.login || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
@@ -133,4 +149,4 @@ const StyledLogin = injectSheet(styles)(
|
|||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(Login)
|
)(Login)
|
||||||
);
|
);
|
||||||
export default StyledLogin;
|
export default withRouter(StyledLogin);
|
||||||
|
|||||||
56
scm-ui/src/containers/Logout.js
Normal file
56
scm-ui/src/containers/Logout.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Redirect } from "react-router-dom";
|
||||||
|
|
||||||
|
import { logout, isAuthenticated } from "../modules/auth";
|
||||||
|
import ErrorPage from "../components/ErrorPage";
|
||||||
|
import Loading from "../components/Loading";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loading: boolean,
|
||||||
|
authenticated: boolean,
|
||||||
|
error?: Error,
|
||||||
|
logout: () => void
|
||||||
|
};
|
||||||
|
|
||||||
|
class Logout extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { authenticated, loading, error } = this.props;
|
||||||
|
// TODO logout is called twice
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
title="Logout failed"
|
||||||
|
subtitle="Something went wrong durring logout"
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (loading || authenticated) {
|
||||||
|
return <Loading />;
|
||||||
|
} else {
|
||||||
|
return <Redirect to="/login" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
let mapped = state.auth.logout || {};
|
||||||
|
mapped.authenticated = isAuthenticated(state);
|
||||||
|
return mapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
logout: () => dispatch(logout())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Logout);
|
||||||
@@ -5,26 +5,36 @@ import { Route, withRouter } from "react-router";
|
|||||||
|
|
||||||
import Repositories from "../repositories/containers/Repositories";
|
import Repositories from "../repositories/containers/Repositories";
|
||||||
import Users from "../users/containers/Users";
|
import Users from "../users/containers/Users";
|
||||||
|
import Login from "../containers/Login";
|
||||||
|
import Logout from "../containers/Logout";
|
||||||
|
|
||||||
import { Switch } from "react-router-dom";
|
import { Switch } from "react-router-dom";
|
||||||
import Footer from "../components/Footer";
|
import ProtectedRoute from "../components/ProtectedRoute";
|
||||||
|
|
||||||
import type { Me } from "../types/me";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
me: Me
|
authenticated?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class Main extends React.Component<Props> {
|
class Main extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { me } = this.props;
|
const { authenticated } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/" component={Repositories} />
|
<ProtectedRoute
|
||||||
<Route path="/users" component={Users} />
|
exact
|
||||||
|
path="/"
|
||||||
|
component={Repositories}
|
||||||
|
authenticated={authenticated}
|
||||||
|
/>
|
||||||
|
<Route exact path="/login" component={Login} />
|
||||||
|
<Route path="/logout" component={Logout} />
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/users"
|
||||||
|
component={Users}
|
||||||
|
authenticated={authenticated}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Footer me={me} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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 auth from "./modules/auth";
|
import auth from "./modules/auth";
|
||||||
import me from "./modules/me";
|
|
||||||
|
|
||||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||||
|
|
||||||
@@ -19,8 +18,7 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
router: routerReducer,
|
router: routerReducer,
|
||||||
repositories,
|
repositories,
|
||||||
users,
|
users,
|
||||||
auth,
|
auth
|
||||||
me
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return createStore(
|
return createStore(
|
||||||
|
|||||||
@@ -1,139 +1,239 @@
|
|||||||
//@flow
|
// @flow
|
||||||
|
import type { Me } from "../types/me";
|
||||||
|
|
||||||
import { apiClient } from "../apiclient";
|
import { apiClient, UNAUTHORIZED_ERROR } from "../apiclient";
|
||||||
import { fetchMe } from "./me";
|
|
||||||
|
|
||||||
|
// Action
|
||||||
|
|
||||||
|
export const LOGIN_REQUEST = "scm/auth/LOGIN_REQUEST";
|
||||||
|
export const LOGIN_SUCCESS = "scm/auth/LOGIN_SUCCESS";
|
||||||
|
export const LOGIN_FAILURE = "scm/auth/LOGIN_FAILURE";
|
||||||
|
|
||||||
|
export const FETCH_ME_REQUEST = "scm/auth/FETCH_ME_REQUEST";
|
||||||
|
export const FETCH_ME_SUCCESS = "scm/auth/FETCH_ME_SUCCESS";
|
||||||
|
export const FETCH_ME_FAILURE = "scm/auth/FETCH_ME_FAILURE";
|
||||||
|
export const FETCH_ME_UNAUTHORIZED = "scm/auth/FETCH_ME_UNAUTHORIZED";
|
||||||
|
|
||||||
|
export const LOGOUT_REQUEST = "scm/auth/LOGOUT_REQUEST";
|
||||||
|
export const LOGOUT_SUCCESS = "scm/auth/LOGOUT_SUCCESS";
|
||||||
|
export const LOGOUT_FAILURE = "scm/auth/LOGOUT_FAILURE";
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
me: { loading: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reducer(state: any = initialState, action: any = {}) {
|
||||||
|
switch (action.type) {
|
||||||
|
case LOGIN_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
login: {
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case LOGIN_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
login: {
|
||||||
|
authenticated: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case LOGIN_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
login: {
|
||||||
|
error: action.payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case FETCH_ME_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
me: {
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case FETCH_ME_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
me: {
|
||||||
|
entry: action.payload
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
authenticated: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case FETCH_ME_UNAUTHORIZED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
me: {},
|
||||||
|
login: {
|
||||||
|
authenticated: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case FETCH_ME_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
me: {
|
||||||
|
error: action.payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case LOGOUT_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logout: {
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case LOGOUT_SUCCESS:
|
||||||
|
return initialState;
|
||||||
|
case LOGOUT_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
logout: {
|
||||||
|
error: action.payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const loginRequest = () => {
|
||||||
|
return {
|
||||||
|
type: LOGIN_REQUEST
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginSuccess = () => {
|
||||||
|
return {
|
||||||
|
type: LOGIN_SUCCESS
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginFailure = (error: Error) => {
|
||||||
|
return {
|
||||||
|
type: LOGIN_FAILURE,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutRequest = () => {
|
||||||
|
return {
|
||||||
|
type: LOGOUT_REQUEST
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutSuccess = () => {
|
||||||
|
return {
|
||||||
|
type: LOGOUT_SUCCESS
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutFailure = (error: Error) => {
|
||||||
|
return {
|
||||||
|
type: LOGOUT_FAILURE,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMeRequest = () => {
|
||||||
|
return {
|
||||||
|
type: FETCH_ME_REQUEST
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMeSuccess = (me: Me) => {
|
||||||
|
return {
|
||||||
|
type: FETCH_ME_SUCCESS,
|
||||||
|
payload: me
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMeUnauthenticated = () => {
|
||||||
|
return {
|
||||||
|
type: FETCH_ME_UNAUTHORIZED
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMeFailure = (error: Error) => {
|
||||||
|
return {
|
||||||
|
type: FETCH_ME_FAILURE,
|
||||||
|
payload: error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// urls
|
||||||
|
|
||||||
|
const ME_URL = "/me";
|
||||||
const LOGIN_URL = "/auth/access_token";
|
const LOGIN_URL = "/auth/access_token";
|
||||||
|
|
||||||
export const LOGIN_REQUEST = "scm/auth/login_request";
|
// side effects
|
||||||
export const LOGIN_SUCCESSFUL = "scm/auth/login_successful";
|
|
||||||
export const LOGIN_FAILED = "scm/auth/login_failed";
|
|
||||||
|
|
||||||
export const LOGOUT_REQUEST = "scm/auth/logout_request";
|
export const login = (username: string, password: string) => {
|
||||||
export const LOGOUT_SUCCESSFUL = "scm/auth/logout_successful";
|
|
||||||
export const LOGOUT_FAILED = "scm/auth/logout_failed";
|
|
||||||
|
|
||||||
export function login(username: string, password: string) {
|
|
||||||
const login_data = {
|
const login_data = {
|
||||||
cookie: true,
|
cookie: true,
|
||||||
grant_type: "password",
|
grant_type: "password",
|
||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
};
|
};
|
||||||
return function(dispatch: any => void) {
|
return function(dispatch: any) {
|
||||||
dispatch(loginRequest());
|
dispatch(loginRequest());
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(LOGIN_URL, login_data)
|
.post(LOGIN_URL, login_data)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// not the best way or?
|
|
||||||
dispatch(fetchMe());
|
dispatch(fetchMe());
|
||||||
dispatch(loginSuccessful());
|
dispatch(loginSuccess());
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
dispatch(loginFailed(err));
|
dispatch(loginFailure(err));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export function loginRequest() {
|
export const fetchMe = () => {
|
||||||
return {
|
return function(dispatch: any) {
|
||||||
type: LOGIN_REQUEST
|
dispatch(fetchMeRequest());
|
||||||
|
return apiClient
|
||||||
|
.get(ME_URL)
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(me => {
|
||||||
|
dispatch(fetchMeSuccess(me));
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
if (error === UNAUTHORIZED_ERROR) {
|
||||||
|
dispatch(fetchMeUnauthenticated());
|
||||||
|
} else {
|
||||||
|
dispatch(fetchMeFailure(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export function loginSuccessful() {
|
export const logout = () => {
|
||||||
return {
|
|
||||||
type: LOGIN_SUCCESSFUL
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loginFailed(error: Error) {
|
|
||||||
return {
|
|
||||||
type: LOGIN_FAILED,
|
|
||||||
payload: error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logout() {
|
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(logoutRequest());
|
dispatch(logoutRequest());
|
||||||
return apiClient
|
return apiClient
|
||||||
.delete(LOGIN_URL)
|
.delete(LOGIN_URL)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(logoutSuccess());
|
dispatch(logoutSuccess());
|
||||||
// not the best way or?
|
|
||||||
dispatch(fetchMe());
|
dispatch(fetchMe());
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
dispatch(logoutFailed(error));
|
dispatch(logoutFailure(error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export function logoutRequest() {
|
// selectors
|
||||||
return {
|
|
||||||
type: LOGOUT_REQUEST
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logoutSuccess() {
|
export const isAuthenticated = (state: any): boolean => {
|
||||||
return {
|
return state.auth && state.auth.login && state.auth.login.authenticated;
|
||||||
type: LOGOUT_SUCCESSFUL
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logoutFailed(error: Error) {
|
|
||||||
return {
|
|
||||||
type: LOGOUT_FAILED,
|
|
||||||
payload: error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function reducer(state: any = {}, action: any = {}) {
|
|
||||||
switch (action.type) {
|
|
||||||
case LOGIN_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: true,
|
|
||||||
login: false,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
case LOGIN_SUCCESSFUL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
login: true,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
case LOGIN_FAILED:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
login: false,
|
|
||||||
error: action.payload
|
|
||||||
};
|
|
||||||
|
|
||||||
case LOGOUT_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: true,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
case LOGOUT_SUCCESSFUL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
login: false,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
case LOGOUT_FAILED:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
error: action.payload
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,115 @@
|
|||||||
// @flow
|
|
||||||
import reducer, {
|
import reducer, {
|
||||||
login,
|
fetchMeSuccess,
|
||||||
logout,
|
logout,
|
||||||
|
logoutSuccess,
|
||||||
|
loginSuccess,
|
||||||
|
fetchMeRequest,
|
||||||
|
loginRequest,
|
||||||
|
logoutRequest,
|
||||||
|
fetchMeFailure,
|
||||||
|
fetchMeUnauthenticated,
|
||||||
|
loginFailure,
|
||||||
|
logoutFailure,
|
||||||
LOGIN_REQUEST,
|
LOGIN_REQUEST,
|
||||||
LOGIN_FAILED,
|
FETCH_ME_REQUEST,
|
||||||
LOGIN_SUCCESSFUL,
|
LOGIN_SUCCESS,
|
||||||
LOGOUT_REQUEST,
|
login,
|
||||||
LOGOUT_SUCCESSFUL,
|
LOGIN_FAILURE,
|
||||||
LOGOUT_FAILED
|
LOGOUT_FAILURE,
|
||||||
|
FETCH_ME_SUCCESS,
|
||||||
|
fetchMe,
|
||||||
|
FETCH_ME_FAILURE,
|
||||||
|
FETCH_ME_UNAUTHORIZED,
|
||||||
|
isAuthenticated
|
||||||
} from "./auth";
|
} from "./auth";
|
||||||
|
|
||||||
import { ME_AUTHENTICATED_REQUEST, ME_AUTHENTICATED_SUCCESS } from "./me";
|
|
||||||
|
|
||||||
import configureMockStore from "redux-mock-store";
|
import configureMockStore from "redux-mock-store";
|
||||||
import thunk from "redux-thunk";
|
import thunk from "redux-thunk";
|
||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
|
import { LOGOUT_REQUEST, LOGOUT_SUCCESS } from "./auth";
|
||||||
|
|
||||||
describe("action tests", () => {
|
describe("auth reducer", () => {
|
||||||
|
it("should initialize in loading state ", () => {
|
||||||
|
const state = reducer();
|
||||||
|
expect(state.me.loading).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set me and login on successful fetch of me", () => {
|
||||||
|
const state = reducer(undefined, fetchMeSuccess({ username: "tricia" }));
|
||||||
|
expect(state.me.loading).toBeFalsy();
|
||||||
|
expect(state.me.entry.username).toBe("tricia");
|
||||||
|
expect(state.login.authenticated).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set authenticated to false", () => {
|
||||||
|
const initialState = {
|
||||||
|
login: {
|
||||||
|
authenticated: true
|
||||||
|
},
|
||||||
|
me: {
|
||||||
|
username: "tricia"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = reducer(initialState, fetchMeUnauthenticated());
|
||||||
|
expect(state.me.username).toBeUndefined();
|
||||||
|
expect(state.login.authenticated).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reset the state after logout", () => {
|
||||||
|
const initialState = {
|
||||||
|
login: {
|
||||||
|
authenticated: true
|
||||||
|
},
|
||||||
|
me: {
|
||||||
|
username: "tricia"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = reducer(initialState, logoutSuccess());
|
||||||
|
expect(state.me.loading).toBeTruthy();
|
||||||
|
expect(state.me.entry).toBeFalsy();
|
||||||
|
expect(state.login).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set state authenticated after login", () => {
|
||||||
|
const state = reducer(undefined, loginSuccess());
|
||||||
|
expect(state.login.authenticated).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set me to loading", () => {
|
||||||
|
const state = reducer({ me: { loading: false } }, fetchMeRequest());
|
||||||
|
expect(state.me.loading).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set login to loading", () => {
|
||||||
|
const state = reducer({ login: { loading: false } }, loginRequest());
|
||||||
|
expect(state.login.loading).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set logout to loading", () => {
|
||||||
|
const state = reducer({ logout: { loading: false } }, logoutRequest());
|
||||||
|
expect(state.logout.loading).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set me to error", () => {
|
||||||
|
const error = new Error("failed");
|
||||||
|
const state = reducer(undefined, fetchMeFailure(error));
|
||||||
|
expect(state.me.error).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set login to error", () => {
|
||||||
|
const error = new Error("failed");
|
||||||
|
const state = reducer(undefined, loginFailure(error));
|
||||||
|
expect(state.login.error).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set logout to error", () => {
|
||||||
|
const error = new Error("failed");
|
||||||
|
const state = reducer(undefined, logoutFailure(error));
|
||||||
|
expect(state.logout.error).toBe(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("auth actions", () => {
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -24,7 +117,7 @@ describe("action tests", () => {
|
|||||||
fetchMock.restore();
|
fetchMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("login success", () => {
|
it("should dispatch login success and dispatch fetch me", () => {
|
||||||
fetchMock.postOnce("/scm/api/rest/v2/auth/access_token", {
|
fetchMock.postOnce("/scm/api/rest/v2/auth/access_token", {
|
||||||
body: {
|
body: {
|
||||||
cookie: true,
|
cookie: true,
|
||||||
@@ -44,8 +137,8 @@ describe("action tests", () => {
|
|||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: LOGIN_REQUEST },
|
{ type: LOGIN_REQUEST },
|
||||||
{ type: ME_AUTHENTICATED_REQUEST },
|
{ type: FETCH_ME_REQUEST },
|
||||||
{ type: LOGIN_SUCCESSFUL }
|
{ type: LOGIN_SUCCESS }
|
||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
@@ -55,23 +148,73 @@ describe("action tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("login failed", () => {
|
it("should dispatch login failure", () => {
|
||||||
fetchMock.postOnce("/scm/api/rest/v2/auth/access_token", {
|
fetchMock.postOnce("/scm/api/rest/v2/auth/access_token", {
|
||||||
status: 400
|
status: 400
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedActions = [{ type: LOGIN_REQUEST }, { type: LOGIN_FAILED }];
|
const expectedActions = [{ type: LOGIN_REQUEST }, { type: LOGIN_FAILURE }];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(login("tricia", "secret123")).then(() => {
|
return store.dispatch(login("tricia", "secret123")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(LOGIN_REQUEST);
|
expect(actions[0].type).toEqual(LOGIN_REQUEST);
|
||||||
expect(actions[1].type).toEqual(LOGIN_FAILED);
|
expect(actions[1].type).toEqual(LOGIN_FAILURE);
|
||||||
expect(actions[1].payload).toBeDefined();
|
expect(actions[1].payload).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("logout success", () => {
|
it("should dispatch fetch me success", () => {
|
||||||
|
fetchMock.getOnce("/scm/api/rest/v2/me", {
|
||||||
|
body: { username: "sorbot" },
|
||||||
|
headers: { "content-type": "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: FETCH_ME_REQUEST },
|
||||||
|
{ type: FETCH_ME_SUCCESS, payload: { username: "sorbot" } }
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
return store.dispatch(fetchMe()).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch fetch me failure", () => {
|
||||||
|
fetchMock.getOnce("/scm/api/rest/v2/me", {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchMe()).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_ME_REQUEST);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_ME_FAILURE);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch fetch me unauthorized", () => {
|
||||||
|
fetchMock.getOnce("/scm/api/rest/v2/me", {
|
||||||
|
status: 401
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: FETCH_ME_REQUEST },
|
||||||
|
{ type: FETCH_ME_UNAUTHORIZED }
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
return store.dispatch(fetchMe()).then(() => {
|
||||||
|
// return of async actions
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch logout success", () => {
|
||||||
fetchMock.deleteOnce("/scm/api/rest/v2/auth/access_token", {
|
fetchMock.deleteOnce("/scm/api/rest/v2/auth/access_token", {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
@@ -82,8 +225,8 @@ describe("action tests", () => {
|
|||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: LOGOUT_REQUEST },
|
{ type: LOGOUT_REQUEST },
|
||||||
{ type: LOGOUT_SUCCESSFUL },
|
{ type: LOGOUT_SUCCESS },
|
||||||
{ type: ME_AUTHENTICATED_REQUEST }
|
{ type: FETCH_ME_REQUEST }
|
||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
@@ -93,68 +236,39 @@ describe("action tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("logout failed", () => {
|
it("should dispatch logout failure", () => {
|
||||||
fetchMock.deleteOnce("/scm/api/rest/v2/auth/access_token", {
|
fetchMock.deleteOnce("/scm/api/rest/v2/auth/access_token", {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedActions = [{ type: LOGOUT_REQUEST }, { type: LOGOUT_FAILED }];
|
const expectedActions = [
|
||||||
|
{ type: LOGOUT_REQUEST },
|
||||||
|
{ type: LOGOUT_FAILURE }
|
||||||
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(logout()).then(() => {
|
return store.dispatch(logout()).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(LOGOUT_REQUEST);
|
expect(actions[0].type).toEqual(LOGOUT_REQUEST);
|
||||||
expect(actions[1].type).toEqual(LOGOUT_FAILED);
|
expect(actions[1].type).toEqual(LOGOUT_FAILURE);
|
||||||
expect(actions[1].payload).toBeDefined();
|
expect(actions[1].payload).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("reducer tests", () => {
|
describe("auth selectors", () => {
|
||||||
test("login request", () => {
|
it("should be false", () => {
|
||||||
var newState = reducer({}, { type: LOGIN_REQUEST });
|
expect(isAuthenticated({})).toBeFalsy();
|
||||||
expect(newState.loading).toBeTruthy();
|
expect(isAuthenticated({ auth: {} })).toBeFalsy();
|
||||||
expect(newState.login).toBeFalsy();
|
expect(isAuthenticated({ auth: { login: {} } })).toBeFalsy();
|
||||||
expect(newState.error).toBeNull();
|
expect(
|
||||||
|
isAuthenticated({ auth: { login: { authenticated: false } } })
|
||||||
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("login successful", () => {
|
it("shuld be true", () => {
|
||||||
var newState = reducer({ login: false }, { type: LOGIN_SUCCESSFUL });
|
expect(
|
||||||
expect(newState.loading).toBeFalsy();
|
isAuthenticated({ auth: { login: { authenticated: true } } })
|
||||||
expect(newState.login).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(newState.error).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("login failed", () => {
|
|
||||||
const err = new Error("error!");
|
|
||||||
var newState = reducer({}, { type: LOGIN_FAILED, payload: err });
|
|
||||||
expect(newState.loading).toBeFalsy();
|
|
||||||
expect(newState.login).toBeFalsy();
|
|
||||||
expect(newState.error).toBe(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("logout request", () => {
|
|
||||||
var newState = reducer({ login: true }, { type: LOGOUT_REQUEST });
|
|
||||||
expect(newState.loading).toBeTruthy();
|
|
||||||
expect(newState.login).toBeTruthy();
|
|
||||||
expect(newState.error).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("logout successful", () => {
|
|
||||||
var newState = reducer({ login: true }, { type: LOGOUT_SUCCESSFUL });
|
|
||||||
expect(newState.loading).toBeFalsy();
|
|
||||||
expect(newState.login).toBeFalsy();
|
|
||||||
expect(newState.error).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("logout failed", () => {
|
|
||||||
const err = new Error("error!");
|
|
||||||
var newState = reducer(
|
|
||||||
{ login: true },
|
|
||||||
{ type: LOGOUT_FAILED, payload: err }
|
|
||||||
);
|
|
||||||
expect(newState.loading).toBeFalsy();
|
|
||||||
expect(newState.login).toBeTruthy();
|
|
||||||
expect(newState.error).toBe(err);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import { apiClient, NOT_AUTHENTICATED_ERROR } from "../apiclient";
|
|
||||||
import type { Me } from "../types/me";
|
|
||||||
|
|
||||||
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: Me) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import reducer, {
|
|
||||||
ME_AUTHENTICATED_REQUEST,
|
|
||||||
ME_AUTHENTICATED_SUCCESS,
|
|
||||||
ME_AUTHENTICATED_FAILURE,
|
|
||||||
ME_UNAUTHENTICATED,
|
|
||||||
fetchMe
|
|
||||||
} from "./me";
|
|
||||||
|
|
||||||
import configureMockStore from "redux-mock-store";
|
|
||||||
import thunk from "redux-thunk";
|
|
||||||
import fetchMock from "fetch-mock";
|
|
||||||
|
|
||||||
describe("fetch tests", () => {
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fetchMock.reset();
|
|
||||||
fetchMock.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("successful me fetch", () => {
|
|
||||||
fetchMock.getOnce("/scm/api/rest/v2/me", {
|
|
||||||
body: { username: "sorbot" },
|
|
||||||
headers: { "content-type": "application/json" }
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: ME_AUTHENTICATED_REQUEST },
|
|
||||||
{ type: ME_AUTHENTICATED_SUCCESS, payload: { username: "sorbot" } }
|
|
||||||
];
|
|
||||||
|
|
||||||
const store = mockStore({});
|
|
||||||
|
|
||||||
return store.dispatch(fetchMe()).then(() => {
|
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("me fetch failed", () => {
|
|
||||||
fetchMock.getOnce("/scm/api/rest/v2/me", {
|
|
||||||
status: 500
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = mockStore({});
|
|
||||||
return store.dispatch(fetchMe()).then(() => {
|
|
||||||
const actions = store.getActions();
|
|
||||||
expect(actions[0].type).toEqual(ME_AUTHENTICATED_REQUEST);
|
|
||||||
expect(actions[1].type).toEqual(ME_AUTHENTICATED_FAILURE);
|
|
||||||
expect(actions[1].payload).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("me fetch unauthenticated", () => {
|
|
||||||
fetchMock.getOnce("/scm/api/rest/v2/me", {
|
|
||||||
status: 401
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: ME_AUTHENTICATED_REQUEST },
|
|
||||||
{ type: ME_UNAUTHENTICATED }
|
|
||||||
];
|
|
||||||
|
|
||||||
const store = mockStore({});
|
|
||||||
|
|
||||||
return store.dispatch(fetchMe()).then(() => {
|
|
||||||
// return of async actions
|
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("reducer tests", () => {
|
|
||||||
test("me request", () => {
|
|
||||||
var newState = reducer({}, { type: ME_AUTHENTICATED_REQUEST });
|
|
||||||
expect(newState.loading).toBeTruthy();
|
|
||||||
expect(newState.me).toBeNull();
|
|
||||||
expect(newState.error).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("fetch me successful", () => {
|
|
||||||
const me = { username: "tricia" };
|
|
||||||
var newState = reducer({}, { type: ME_AUTHENTICATED_SUCCESS, payload: me });
|
|
||||||
expect(newState.loading).toBeFalsy();
|
|
||||||
expect(newState.me).toBe(me);
|
|
||||||
expect(newState.error).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("fetch me failed", () => {
|
|
||||||
const err = new Error("error!");
|
|
||||||
var newState = reducer(
|
|
||||||
{},
|
|
||||||
{ type: ME_AUTHENTICATED_FAILURE, payload: err }
|
|
||||||
);
|
|
||||||
expect(newState.loading).toBeFalsy();
|
|
||||||
expect(newState.me).toBeNull();
|
|
||||||
expect(newState.error).toBe(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("me unauthenticated", () => {
|
|
||||||
var newState = reducer({}, { type: ME_UNAUTHENTICATED });
|
|
||||||
expect(newState.loading).toBeFalsy();
|
|
||||||
expect(newState.me).toBeNull();
|
|
||||||
expect(newState.error).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { apiClient, PAGE_NOT_FOUND_ERROR } from "../../apiclient";
|
import { apiClient, NOT_FOUND_ERROR } from "../../apiclient";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
import { ThunkDispatch } from "redux-thunk";
|
import { ThunkDispatch } from "redux-thunk";
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export function fetchUsers() {
|
|||||||
dispatch(fetchUsersSuccess(data));
|
dispatch(fetchUsersSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err === PAGE_NOT_FOUND_ERROR) {
|
if (err === NOT_FOUND_ERROR) {
|
||||||
dispatch(usersNotFound(USERS_URL));
|
dispatch(usersNotFound(USERS_URL));
|
||||||
} else {
|
} else {
|
||||||
dispatch(failedToFetchUsers(USERS_URL, err));
|
dispatch(failedToFetchUsers(USERS_URL, err));
|
||||||
|
|||||||
Reference in New Issue
Block a user