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
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import Logo from "./Logo";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -6,6 +6,7 @@ type Props = {
|
||||
label?: string,
|
||||
placeholder?: string,
|
||||
type?: string,
|
||||
autofocus?: boolean,
|
||||
onChange: string => void
|
||||
};
|
||||
|
||||
@@ -15,6 +16,14 @@ class InputField extends React.Component<Props> {
|
||||
placeholder: ""
|
||||
};
|
||||
|
||||
field: ?HTMLInputElement;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.autofocus && this.field) {
|
||||
this.field.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
@@ -35,6 +44,9 @@ class InputField extends React.Component<Props> {
|
||||
{this.renderLabel()}
|
||||
<div className="control">
|
||||
<input
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
}}
|
||||
className="input"
|
||||
type={type}
|
||||
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 Image from "../images/logo.png";
|
||||
|
||||
class Logo extends React.PureComponent {
|
||||
type Props = {};
|
||||
|
||||
class Logo extends React.Component<Props> {
|
||||
render() {
|
||||
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 PrimaryNavigationLink from "./PrimaryNavigationLink";
|
||||
|
||||
type Props = {};
|
||||
|
||||
class PrimaryNavigation extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { Route, Link } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -5,30 +5,36 @@ import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
value: string,
|
||||
disabled: boolean,
|
||||
isLoading: boolean,
|
||||
large?: boolean,
|
||||
fullWidth?: boolean
|
||||
};
|
||||
|
||||
class SubmitButton extends React.Component<Props> {
|
||||
render() {
|
||||
const { value, large, fullWidth } = this.props;
|
||||
const { value, large, fullWidth, isLoading, disabled } = this.props;
|
||||
|
||||
const largeClass = large ? "is-large" : "";
|
||||
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
|
||||
const loadingClass = isLoading ? "is-loading" : "";
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input
|
||||
<button
|
||||
type="submit"
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
"button",
|
||||
"is-link",
|
||||
largeClass,
|
||||
fullWidthClass
|
||||
fullWidthClass,
|
||||
loadingClass
|
||||
)}
|
||||
value={value}
|
||||
/>
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,45 +1,50 @@
|
||||
import React, { Component } from "react";
|
||||
import Main from "./Main";
|
||||
import Login from "./Login";
|
||||
import { getIsAuthenticated } from "../modules/login";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { ThunkDispatch } from "redux-thunk";
|
||||
import { fetchMe } from "../modules/me";
|
||||
|
||||
import "./App.css";
|
||||
import Header from "../components/Header";
|
||||
import PrimaryNavigation from "../components/PrimaryNavigation";
|
||||
import Loading from "../components/Loading";
|
||||
import Notification from "../components/Notification";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
type Props = {
|
||||
login: boolean,
|
||||
username: string,
|
||||
getAuthState: () => void,
|
||||
me: any,
|
||||
fetchMe: () => void,
|
||||
loading: boolean
|
||||
};
|
||||
|
||||
class App extends Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.getAuthState();
|
||||
this.props.fetchMe();
|
||||
}
|
||||
render() {
|
||||
const { login, loading } = this.props;
|
||||
const { me, loading } = this.props;
|
||||
|
||||
let content;
|
||||
let content = [];
|
||||
let navigation;
|
||||
|
||||
if (loading) {
|
||||
content = <div>Loading...</div>;
|
||||
} else if (!login) {
|
||||
content = <Login />;
|
||||
content.push(<Loading />);
|
||||
} else if (!me) {
|
||||
content.push(<Login />);
|
||||
} else {
|
||||
content = <Main />;
|
||||
content.push(<Main />, <Footer me={me} />);
|
||||
navigation = <PrimaryNavigation />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<Header>{navigation}</Header>
|
||||
{content}
|
||||
{content.map(c => {
|
||||
return c;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -47,12 +52,12 @@ class App extends Component<Props> {
|
||||
|
||||
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
|
||||
return {
|
||||
getAuthState: () => dispatch(getIsAuthenticated())
|
||||
fetchMe: () => dispatch(fetchMe())
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return state.login || {};
|
||||
return state.me || {};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
|
||||
@@ -9,11 +9,9 @@ import SubmitButton from "../components/SubmitButton";
|
||||
|
||||
import classNames from "classnames";
|
||||
import Avatar from "../images/blib.jpg";
|
||||
import ErrorNotification from "../components/ErrorNotification";
|
||||
|
||||
const styles = {
|
||||
spacing: {
|
||||
paddingTop: "5rem"
|
||||
},
|
||||
avatar: {
|
||||
marginTop: "-70px",
|
||||
paddingBottom: "20px"
|
||||
@@ -32,6 +30,8 @@ const styles = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
classes: any,
|
||||
login: (username: string, password: string) => void
|
||||
};
|
||||
@@ -55,16 +55,26 @@ class Login extends React.Component<Props, State> {
|
||||
this.setState({ password: value });
|
||||
};
|
||||
|
||||
handleSubmit(event: Event) {
|
||||
handleSubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (this.isValid()) {
|
||||
this.props.login(this.state.username, this.state.password);
|
||||
}
|
||||
};
|
||||
|
||||
isValid() {
|
||||
return this.state.username && this.state.password;
|
||||
}
|
||||
|
||||
isInValid() {
|
||||
return !this.isValid();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const { classes, loading, error } = this.props;
|
||||
return (
|
||||
<section className="hero is-fullheight">
|
||||
<div className={classes.spacing}>
|
||||
<section className="hero has-background-light">
|
||||
<div className="hero-body">
|
||||
<div className="container has-text-centered">
|
||||
<div className="column is-4 is-offset-4">
|
||||
<h3 className="title">Login</h3>
|
||||
@@ -77,9 +87,11 @@ class Login extends React.Component<Props, State> {
|
||||
alt="SCM-Manager"
|
||||
/>
|
||||
</figure>
|
||||
<form onSubmit={this.handleSubmit.bind(this)}>
|
||||
<ErrorNotification error={error} />
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<InputField
|
||||
placeholder="Your Username"
|
||||
autofocus={true}
|
||||
onChange={this.handleUsernameChange}
|
||||
/>
|
||||
<InputField
|
||||
@@ -87,7 +99,12 @@ class Login extends React.Component<Props, State> {
|
||||
type="password"
|
||||
onChange={this.handlePasswordChange}
|
||||
/>
|
||||
<SubmitButton value="Login" fullWidth={true} />
|
||||
<SubmitButton
|
||||
value="Login"
|
||||
disabled={this.isInValid()}
|
||||
fullWidth={true}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +116,7 @@ class Login extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {};
|
||||
return state.login || {};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
|
||||
import repositories from "./repositories/modules/repositories";
|
||||
import users from "./users/modules/users";
|
||||
import login from "./modules/login";
|
||||
import me from "./modules/me";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
|
||||
@@ -18,7 +19,8 @@ function createReduxStore(history: BrowserHistory) {
|
||||
router: routerReducer,
|
||||
repositories,
|
||||
users,
|
||||
login
|
||||
login,
|
||||
me
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
import { apiClient, NOT_AUTHENTICATED_ERROR } from "../apiclient";
|
||||
import { fetchMe } from "./me";
|
||||
|
||||
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_SUCCESSFUL = "scm/auth/login_successful";
|
||||
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() {
|
||||
return {
|
||||
type: GET_IS_AUTHENTICATED_REQUEST
|
||||
export function login(username: string, password: string) {
|
||||
const login_data = {
|
||||
cookie: true,
|
||||
grant_type: "password",
|
||||
username,
|
||||
password
|
||||
};
|
||||
}
|
||||
|
||||
export function getIsAuthenticated() {
|
||||
return function(dispatch: any => void) {
|
||||
dispatch(getIsAuthenticatedRequest());
|
||||
dispatch(loginRequest());
|
||||
return apiClient
|
||||
.get(AUTHENTICATION_INFO_URL)
|
||||
.post(LOGIN_URL, login_data)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
// not the best way or?
|
||||
dispatch(fetchMe());
|
||||
dispatch(loginSuccessful());
|
||||
})
|
||||
.then(data => {
|
||||
if (data) {
|
||||
dispatch(isAuthenticated(data.username));
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (error === NOT_AUTHENTICATED_ERROR) {
|
||||
dispatch(isNotAuthenticated());
|
||||
} else {
|
||||
// TODO: Handle errors other than not_authenticated
|
||||
}
|
||||
.catch(err => {
|
||||
dispatch(loginFailed(err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function isAuthenticated(username: string) {
|
||||
return {
|
||||
type: IS_AUTHENTICATED,
|
||||
username
|
||||
};
|
||||
}
|
||||
|
||||
export function isNotAuthenticated() {
|
||||
return {
|
||||
type: IS_NOT_AUTHENTICATED
|
||||
};
|
||||
}
|
||||
|
||||
export function loginRequest() {
|
||||
return {
|
||||
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() {
|
||||
return {
|
||||
type: LOGIN_SUCCESSFUL
|
||||
};
|
||||
}
|
||||
|
||||
export default function reducer(
|
||||
state: any = { loading: true },
|
||||
action: any = {}
|
||||
) {
|
||||
export function loginFailed(error: Error) {
|
||||
return {
|
||||
type: LOGIN_FAILED,
|
||||
payload: error
|
||||
};
|
||||
}
|
||||
|
||||
export default function reducer(state: any = {}, action: any = {}) {
|
||||
switch (action.type) {
|
||||
case LOGIN:
|
||||
case LOGIN_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
@@ -112,21 +73,6 @@ export default function reducer(
|
||||
login: false,
|
||||
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:
|
||||
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