Client: login/register
Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register
This commit is contained in:
parent
166cbd201a
commit
3b13ee67c0
@ -32,3 +32,16 @@ if app.debug:
|
||||
).handlers = logging.getLogger('werkzeug').handlers
|
||||
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
|
||||
appLog.setLevel(logging.DEBUG)
|
||||
|
||||
if app.debug :
|
||||
# Enable CORS
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||
response.headers.add(
|
||||
'Access-Control-Allow-Headers', 'Content-Type,Authorization'
|
||||
)
|
||||
response.headers.add(
|
||||
'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH,OPTIONS'
|
||||
)
|
||||
return response
|
||||
|
@ -191,3 +191,42 @@ class TestAuthBlueprint(BaseTestCase):
|
||||
self.assertTrue(
|
||||
data['message'] == 'Invalid token. Please log in again.')
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_user_profile(self):
|
||||
add_user('test', 'test@test.com', 'test')
|
||||
with self.client:
|
||||
resp_login = self.client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(
|
||||
email='test@test.com',
|
||||
password='test'
|
||||
)),
|
||||
content_type='application/json'
|
||||
)
|
||||
response = self.client.get(
|
||||
'/api/auth/profile',
|
||||
headers=dict(
|
||||
Authorization='Bearer ' + json.loads(
|
||||
resp_login.data.decode()
|
||||
)['auth_token']
|
||||
)
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
self.assertTrue(data['status'] == 'success')
|
||||
self.assertTrue(data['data'] is not None)
|
||||
self.assertTrue(data['data']['username'] == 'test')
|
||||
self.assertTrue(data['data']['email'] == 'test@test.com')
|
||||
self.assertTrue(data['data']['created_at'])
|
||||
self.assertFalse(data['data']['admin'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_invalid_profile(self):
|
||||
with self.client:
|
||||
response = self.client.get(
|
||||
'/api/auth/profile',
|
||||
headers=dict(Authorization='Bearer invalid'))
|
||||
data = json.loads(response.data.decode())
|
||||
self.assertTrue(data['status'] == 'error')
|
||||
self.assertTrue(
|
||||
data['message'] == 'Invalid token. Please log in again.')
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
@ -4,6 +4,7 @@ from sqlalchemy import exc, or_
|
||||
from mpwo_api import appLog, bcrypt, db
|
||||
|
||||
from .models import User
|
||||
from .utils import authenticate
|
||||
|
||||
auth_blueprint = Blueprint('auth', __name__)
|
||||
|
||||
@ -12,7 +13,9 @@ auth_blueprint = Blueprint('auth', __name__)
|
||||
def register_user():
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
if not post_data:
|
||||
if not post_data or post_data.get('username') is None \
|
||||
or post_data.get('email') is None \
|
||||
or post_data.get('password') is None:
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Invalid payload.'
|
||||
@ -52,9 +55,10 @@ def register_user():
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Invalid payload.'
|
||||
'message': 'Error. Please try again or contact the administrator.'
|
||||
}
|
||||
return jsonify(response_object), 400
|
||||
|
||||
@ -95,19 +99,20 @@ def login_user():
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Try again'
|
||||
'message': 'Error. Please try again or contact the administrator.'
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/logout', methods=['GET'])
|
||||
def logout_user():
|
||||
@authenticate
|
||||
def logout_user(user_id):
|
||||
# get auth token
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if auth_header:
|
||||
auth_token = auth_header.split(" ")[1]
|
||||
resp = User.decode_auth_token(auth_token)
|
||||
if not isinstance(resp, str):
|
||||
if not isinstance(user_id, str):
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'Successfully logged out.'
|
||||
@ -125,3 +130,20 @@ def logout_user():
|
||||
'message': 'Provide a valid auth token.'
|
||||
}
|
||||
return jsonify(response_object), 403
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile', methods=['GET'])
|
||||
@authenticate
|
||||
def get_user_status(user_id):
|
||||
user = User.query.filter_by(id=user_id).first()
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'created_at': user.created_at,
|
||||
'admin': user.admin,
|
||||
}
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
|
35
mpwo_api/mpwo_api/users/utils.py
Normal file
35
mpwo_api/mpwo_api/users/utils.py
Normal file
@ -0,0 +1,35 @@
|
||||
from functools import wraps
|
||||
|
||||
from flask import request, jsonify
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
def authenticate(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Something went wrong. Please contact us.'
|
||||
}
|
||||
code = 401
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header:
|
||||
response_object['message'] = 'Provide a valid auth token.'
|
||||
code = 403
|
||||
return jsonify(response_object), code
|
||||
auth_token = auth_header.split(" ")[1]
|
||||
resp = User.decode_auth_token(auth_token)
|
||||
if isinstance(resp, str):
|
||||
response_object['message'] = resp
|
||||
return jsonify(response_object), code
|
||||
user = User.query.filter_by(id=resp).first()
|
||||
if not user:
|
||||
return jsonify(response_object), code
|
||||
return f(resp, *args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def is_admin(user_id):
|
||||
user = User.query.filter_by(id=user_id).first()
|
||||
return user.admin
|
@ -0,0 +1,117 @@
|
||||
import mpwoApi from '../mpwoApi'
|
||||
|
||||
function AuthError(message) {
|
||||
return { type: 'AUTH_ERROR', message }
|
||||
}
|
||||
|
||||
function ProfileSuccess(message) {
|
||||
return { type: 'PROFILE_SUCCESS', message }
|
||||
}
|
||||
|
||||
function ProfileError(message) {
|
||||
return { type: 'PROFILE_ERROR', message }
|
||||
}
|
||||
|
||||
const updateFormDataEmail = value => ({
|
||||
type: 'UPDATE_FORMDATA_EMAIL',
|
||||
email: value,
|
||||
})
|
||||
|
||||
const updateFormDataUsername = value => ({
|
||||
type: 'UPDATE_FORMDATA_USERNAME',
|
||||
username: value,
|
||||
})
|
||||
|
||||
const updateFormDataPassword = value => ({
|
||||
type: 'UPDATE_FORMDATA_PASSWORD',
|
||||
password: value,
|
||||
})
|
||||
|
||||
export function getProfile(dispatch) {
|
||||
return mpwoApi
|
||||
.getProfile()
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(ProfileSuccess(ret))
|
||||
} else {
|
||||
dispatch(ProfileError(ret.message))
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function register(formData) {
|
||||
return function(dispatch) {
|
||||
return mpwoApi
|
||||
.register(formData.username, formData.email, formData.password)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
window.localStorage.setItem('authToken', ret.auth_token)
|
||||
getProfile(dispatch)
|
||||
} else {
|
||||
dispatch(AuthError(ret.message))
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function login(formData) {
|
||||
return function(dispatch) {
|
||||
return mpwoApi
|
||||
.login(formData.email, formData.password)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
window.localStorage.setItem('authToken', ret.auth_token)
|
||||
getProfile(dispatch)
|
||||
} else {
|
||||
dispatch(AuthError(ret.message))
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function loadProfile() {
|
||||
if (window.localStorage.getItem('authToken')) {
|
||||
return function(dispatch) {
|
||||
getProfile(dispatch)
|
||||
}
|
||||
}
|
||||
return { type: 'LOGOUT' }
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return { type: 'LOGOUT' }
|
||||
}
|
||||
|
||||
export function handleUserFormSubmit(event, formType) {
|
||||
event.preventDefault()
|
||||
return (dispatch, getState) => {
|
||||
const state = getState()
|
||||
let { formData } = state.formData
|
||||
formData.formData = state.formData.formData
|
||||
if (formType === 'Login') {
|
||||
dispatch(login(formData.formData))
|
||||
} else { // formType === 'Register'
|
||||
dispatch(register(formData.formData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handleFormChange = event => dispatch => {
|
||||
switch (event.target.name) {
|
||||
case 'email':
|
||||
return dispatch(updateFormDataEmail(event.target.value))
|
||||
case 'username':
|
||||
return dispatch(updateFormDataUsername(event.target.value))
|
||||
default: // case 'password':
|
||||
return dispatch(updateFormDataPassword(event.target.value))
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
|
||||
import './App.css'
|
||||
import Logout from './Logout'
|
||||
import NavBar from './NavBar'
|
||||
import UserForm from './User/UserForm'
|
||||
|
||||
export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
@ -13,9 +16,37 @@ export default class App extends React.Component {
|
||||
return (
|
||||
<div className="App">
|
||||
<NavBar />
|
||||
<p className="App-body">
|
||||
App in progress
|
||||
</p>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<br />
|
||||
<Switch>
|
||||
<Route
|
||||
exact path="/register"
|
||||
render={() => (
|
||||
<UserForm
|
||||
formType={'Register'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact path="/login"
|
||||
render={() => (
|
||||
<UserForm
|
||||
formType={'Login'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact path="/logout"
|
||||
render={() => (
|
||||
<Logout />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
31
mpwo_client/src/components/Logout.jsx
Normal file
31
mpwo_client/src/components/Logout.jsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { logout } from '../actions'
|
||||
|
||||
class Logout extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.UserLogout()
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
You are now logged out.
|
||||
Click <Link to="/login">here</Link> to log back in.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
UserLogout: () => {
|
||||
dispatch(logout())
|
||||
}
|
||||
})
|
||||
)(Logout)
|
@ -1,14 +1,9 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default class NavBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.props = props
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
function NavBar (props) {
|
||||
return (
|
||||
<header>
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<span className="navbar-brand">mpwo</span>
|
||||
@ -36,10 +31,50 @@ export default class NavBar extends React.Component {
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
{!props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/login',
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{!props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/register',
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/logout',
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
export default connect(
|
||||
state => ({
|
||||
user: state.user,
|
||||
})
|
||||
)(NavBar)
|
||||
|
52
mpwo_client/src/components/User/Form.jsx
Normal file
52
mpwo_client/src/components/User/Form.jsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
|
||||
|
||||
export default function Form (props) {
|
||||
return (
|
||||
<div>
|
||||
<h1>{props.formType}</h1>
|
||||
<hr /><br />
|
||||
<form onSubmit={event =>
|
||||
props.handleUserFormSubmit(event, props.formType)}
|
||||
>
|
||||
{props.formType === 'Register' &&
|
||||
<div className="form-group">
|
||||
<input
|
||||
name="username"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
placeholder="Enter a username"
|
||||
required
|
||||
onChange={props.onHandleFormChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className="form-group">
|
||||
<input
|
||||
name="email"
|
||||
className="form-control input-lg"
|
||||
type="email"
|
||||
placeholder="Enter an email address"
|
||||
required
|
||||
onChange={props.onHandleFormChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
name="password"
|
||||
className="form-control input-lg"
|
||||
type="password"
|
||||
placeholder="Enter a password"
|
||||
required
|
||||
onChange={props.onHandleFormChange}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
value="Submit"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
36
mpwo_client/src/components/User/UserForm.jsx
Normal file
36
mpwo_client/src/components/User/UserForm.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Form from './Form'
|
||||
import { handleFormChange, handleUserFormSubmit } from '../../actions'
|
||||
|
||||
function UserForm(props) {
|
||||
return (
|
||||
<div>
|
||||
{props.message !== '' && (
|
||||
<code>{props.message}</code>
|
||||
)}
|
||||
<Form
|
||||
formType={props.formType}
|
||||
userForm={props.formData}
|
||||
onHandleFormChange={event => props.onHandleFormChange(event)}
|
||||
handleUserFormSubmit={(event, formType) =>
|
||||
props.onHandleUserFormSubmit(event, formType)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default connect(
|
||||
state => ({
|
||||
formData: state.formData,
|
||||
message: state.message,
|
||||
}),
|
||||
dispatch => ({
|
||||
onHandleFormChange: event => {
|
||||
dispatch(handleFormChange(event))
|
||||
},
|
||||
onHandleUserFormSubmit: (event, formType) => {
|
||||
dispatch(handleUserFormSubmit(event, formType))
|
||||
},
|
||||
})
|
||||
)(UserForm)
|
@ -10,7 +10,7 @@ import App from './components/App'
|
||||
import Root from './components/Root'
|
||||
import registerServiceWorker from './registerServiceWorker'
|
||||
import reducers from './reducers'
|
||||
|
||||
import { loadProfile } from './actions'
|
||||
|
||||
export const history = createBrowserHistory()
|
||||
|
||||
@ -24,6 +24,10 @@ export const store = createStore(
|
||||
)
|
||||
)
|
||||
|
||||
if (window.localStorage.getItem('authToken') !== null) {
|
||||
store.dispatch(loadProfile())
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Root store={store} history={history}>
|
||||
<App />
|
||||
|
48
mpwo_client/src/mpwoApi.js
Normal file
48
mpwo_client/src/mpwoApi.js
Normal file
@ -0,0 +1,48 @@
|
||||
const apiUrl = `${process.env.REACT_APP_API_URL}`
|
||||
|
||||
|
||||
export default class MpwoApi {
|
||||
static login(email, password) {
|
||||
const request = new Request(`${apiUrl}auth/login`, {
|
||||
method: 'POST',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password,
|
||||
}),
|
||||
})
|
||||
return fetch(request)
|
||||
.then(response => response.json())
|
||||
.catch(error => error)
|
||||
}
|
||||
static register(username, email, password) {
|
||||
const request = new Request(`${apiUrl}auth/register`, {
|
||||
method: 'POST',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
}),
|
||||
})
|
||||
return fetch(request)
|
||||
.then(response => response.json())
|
||||
.catch(error => error)
|
||||
}
|
||||
static getProfile() {
|
||||
const request = new Request(`${apiUrl}auth/profile`, {
|
||||
method: 'GET',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${window.localStorage.getItem('authToken')}`,
|
||||
}),
|
||||
})
|
||||
return fetch(request)
|
||||
.then(response => response.json())
|
||||
.catch(error => error)
|
||||
}
|
||||
}
|
@ -1,9 +1,76 @@
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
import user from './user'
|
||||
import initial from './initial'
|
||||
|
||||
const message = (state = initial.message, action) => {
|
||||
switch (action.type) {
|
||||
case 'AUTH_ERROR':
|
||||
case 'PROFILE_ERROR':
|
||||
return action.message
|
||||
case 'LOGOUT':
|
||||
return ''
|
||||
case 'PROFILE_SUCCESS':
|
||||
return ''
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const user = (state = initial.user, action) => {
|
||||
switch (action.type) {
|
||||
case 'AUTH_ERROR':
|
||||
case 'PROFILE_ERROR':
|
||||
case 'LOGOUT':
|
||||
window.localStorage.removeItem('authToken')
|
||||
return initial.user
|
||||
case 'PROFILE_SUCCESS':
|
||||
return {
|
||||
id: action.message.data.id,
|
||||
username: action.message.data.username,
|
||||
email: action.message.data.email,
|
||||
isAdmin: action.message.data.admin,
|
||||
createdAt: action.message.data.created_at,
|
||||
isAuthenticated: true
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const formData = (state = initial.formData, action) => {
|
||||
switch (action.type) {
|
||||
case 'UPDATE_FORMDATA_EMAIL':
|
||||
return {
|
||||
formData: {
|
||||
...state.formData,
|
||||
email: action.email
|
||||
},
|
||||
}
|
||||
case 'UPDATE_FORMDATA_USERNAME':
|
||||
return {
|
||||
formData: {
|
||||
...state.formData,
|
||||
username: action.username
|
||||
},
|
||||
}
|
||||
case 'UPDATE_FORMDATA_PASSWORD':
|
||||
return {
|
||||
formData: {
|
||||
...state.formData,
|
||||
password: action.password
|
||||
},
|
||||
}
|
||||
case 'PROFILE_SUCCESS':
|
||||
return initial.formData
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const reducers = combineReducers({
|
||||
message,
|
||||
user,
|
||||
formData,
|
||||
})
|
||||
|
||||
export default reducers
|
||||
|
18
mpwo_client/src/reducers/initial.js
Normal file
18
mpwo_client/src/reducers/initial.js
Normal file
@ -0,0 +1,18 @@
|
||||
export default {
|
||||
message: '',
|
||||
user: {
|
||||
id: '',
|
||||
username: '',
|
||||
email: '',
|
||||
createdAt: '',
|
||||
isAdmin: false,
|
||||
isAuthenticated: false,
|
||||
},
|
||||
formData: {
|
||||
formData: {
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
const user = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case 'AUTH_ERROR':
|
||||
case 'PROFILE_ERROR':
|
||||
case 'LOGOUT':
|
||||
window.localStorage.removeItem('authToken')
|
||||
return null
|
||||
case 'PROFILE_SUCCESS':
|
||||
return {
|
||||
id: action.message.data.id,
|
||||
username: action.message.data.username,
|
||||
email: action.message.data.email,
|
||||
isAdmin: action.message.data.is_admin,
|
||||
createdAt: action.message.data.created_at,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default user
|
Loading…
Reference in New Issue
Block a user