MERN Stack with Mongoose 和 Heroku 503 在注销用户时
MERN Stack with Mongoose and Heroku 503 on logging out a user
我正在构建一个带有用户身份验证的 React Native 应用程序。每次我 'logout' 一个用户(使用 JWT)。我注销了,然后在 x 秒后抛出 503。和 Heroku 崩溃,我每次都必须手动重启 Heroku Dyno,(或等待 10 分钟)
包:
后端:
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.2.6",
"morgan": "^1.10.0",
"multer": "^1.4.4",
"nodemon": "^2.0.15"
}
前端:
"dependencies": {
"@react-native-async-storage/async-storage": "~1.15.0",
"@react-navigation/bottom-tabs": "^6.2.0",
"@react-navigation/material-top-tabs": "^6.2.1",
"@react-navigation/native": "^6.0.8",
"@react-navigation/stack": "^6.2.1",
"axios": "^0.26.1",
"expo": "~44.0.0",
"expo-status-bar": "~1.2.0",
"jwt-decode": "^3.1.2",
"native-base": "^2.13.14",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3",
"react-native-gesture-handler": "~2.1.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-reanimated": "~2.3.1",
"react-native-safe-area-context": "3.3.2",
"react-native-screens": "~3.10.1",
"react-native-swipe-list-view": "^3.2.9",
"react-native-swiper": "^1.6.0",
"react-native-tab-view": "^3.1.1",
"react-native-toast-message": "^2.1.5",
"react-native-vector-icons": "^9.1.0",
"react-native-web": "0.17.1",
"react-redux": "^7.2.8",
"redux": "^4.1.2",
"redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.1",
"undefined": "@react-native/community/masked-view"
},
"devDependencies": {
"@babel/core": "^7.12.9"
},
"private": true
}
后端App.js
const express = require('express');
const app = express();
require('dotenv/config');
const morgan = require('morgan');
const mongoose = require('mongoose');
const cors = require('cors');
const errorHandler = require('./helpers/error-handler');
//cors
app.use(cors());
app.options('*', cors());
const productsRouter = require('./routers/products');
const categoriesRouter = require('./routers/categories');
const ordersRouter = require('./routers/orders');
const usersRouter = require('./routers/users');
const authJwt = require('./helpers/jwt');
const api = process.env.API_URL;
// MiddleWare
app.use(express.json());
app.use(morgan('tiny'));
app.use(authJwt());
app.use('/public/uploads', express.static(__dirname + '/public/uploads'));
app.use(errorHandler);
// Routers
app.use(`${api}/categories`, categoriesRouter);
app.use(`${api}/orders`, ordersRouter);
app.use(`${api}/products`, productsRouter);
app.use(`${api}/users`, usersRouter);
mongoose
.connect(process.env.CONNECTION_STRING)
.then(() => {
console.log('database connection is ready');
})
.catch((err) => {
console.log(err);
});
// Development
// app.listen(3000, () => {
// console.log('JamComMobile BE running on http://localhost:3000');
// });
// Production
let server = app.listen(process.env.PORT || 4000, function () {
var port = server.address().port;
console.log('Express is working on port ' + port)
})
server.on('clientError', (err, socket) => {
console.error(err);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
后端用户模型
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
passwordHash: {
type: String,
required: true,
},
phone: {
type: String,
required: true,
},
isAdmin: {
type: Boolean,
default: false,
},
street: {
type: String,
default: ''
},
apartment: {
type: String,
default: ''
},
zip :{
type: String,
default: ''
},
city: {
type: String,
default: ''
},
country: {
type: String,
default: ''
}
});
userSchema.virtual('id').get(function () {
return this._id.toHexString();
});
userSchema.set('toJSON', {
virtuals: true,
});
exports.User = mongoose.model('User', userSchema);
exports.userSchema = userSchema;
后端jwt.js
const expressJwt = require('express-jwt');
function authJwt() {
const secret = process.env.secret;
const api = process.env.API_URL;
return expressJwt({
secret,
algorithms: ['HS256'],
isRevoked: isRevoked,
}).unless({
path: [
{ url: /\/public\/uploads(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/products(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/categories(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/orders(.*)/, methods: ['GET', 'OPTIONS', 'POST'] },
// { url: /\/api\/v1\/users(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/users\/.*/, methods: ['GET'] },
`${api}/users/login`,
`${api}/users/register`,
],
});
}
//TODO: need to clean this up and add additional roles
//this rejects the api call if user is not admin
// async function isRevoked(req, payload, done) {
// if (!payload.isAdmin) {
// done(null, true);
// }
// done();
// }
async function isRevoked(req, token) { // token now contains payload data
if(!token.payload.isAdmin) {
return true // if the isAdmin flag in payload is false, then we reject the token
}
return false ;
}
module.exports = authJwt;
前端UserProfile.js(使用注销方法)
import React, { useContext, useState, useCallback } from 'react';
import { View, Text, ScrollView, StyleSheet, Button } from 'react-native';
import { Container } from 'native-base';
import { useFocusEffect } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import axios from 'axios';
import baseURL from '../../assets/common/baseUrl';
import AuthGlobal from '../../Context/store/AuthGlobal';
import { logoutUser } from '../../Context/actions/auth.actions';
const UserProfile = (props) => {
const context = useContext(AuthGlobal);
const [userProfile, setUserProfile] = useState();
useFocusEffect(
useCallback(() => {
if (
context.stateUser.isAuthenticated === false ||
context.stateUser.isAuthenticated === null
) {
props.navigation.navigate('Login');
}
AsyncStorage.getItem('jwt')
.then((res) => {
axios
.get(`${baseURL}users/${context.stateUser.user.userId}`, {
headers: { Authorization: `Bearer ${res}` },
})
.then((user) => setUserProfile(user.data));
})
.catch((error) => console.log(error));
return () => {
setUserProfile();
};
}, [context.stateUser.isAuthenticated]),
);
return (
<Container style={styles.container}>
<ScrollView contentContainerStyle={styles.subContainer}>
<Text style={{ fontSize: 30 }}>
{userProfile ? userProfile.name : ''}
</Text>
<View style={{ marginTop: 20 }}>
<Text style={{ margin: 10 }}>
Email: {userProfile ? userProfile.email : ''}
</Text>
<Text style={{ margin: 10 }}>
Phone: {userProfile ? userProfile.phone : ''}
</Text>
</View>
<View style={{ marginTop: 80 }}>
<Button
title={'Sign Out'}
onPress={() => [
logoutUser(context.dispatch),
AsyncStorage.removeItem('jwt').catch(
(error) => console.log(error)
),
]}
/>
</View>
</ScrollView>
</Container>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
},
subContainer: {
alignItems: 'center',
marginTop: 60,
},
});
export default UserProfile;
React 上下文 API AuthActions.js
import jwt_decode from 'jwt-decode';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Toast from 'react-native-toast-message';
import baseURL from '../../assets/common/baseUrl';
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
export const loginUser = (user, dispatch) => {
fetch(`${baseURL}users/login`, {
method: 'POST',
body: JSON.stringify(user),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((data) => {
if (data) {
const token = data.token;
AsyncStorage.setItem('jwt', token);
const decoded = jwt_decode(token);
dispatch(setCurrentUser(decoded, user));
} else {
logoutUser(dispatch);
}
})
.catch((err) => {
Toast.show({
topOffset: 60,
type: 'error',
text1: 'Please provide correct credentials in order to login',
});
logoutUser(dispatch);
});
};
export const getUserProfile = (id) => {
fetch(`${baseURL}users/${id}`, {
method: 'GET',
body: JSON.stringify(user),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((data) => console.log(data));
};
export const logoutUser = (dispatch) => {
AsyncStorage.removeItem('jwt');
dispatch(setCurrentUser({}));
};
export const setCurrentUser = (decoded, user) => {
return {
type: SET_CURRENT_USER,
payload: decoded,
userProfile: user,
};
};
AuthReducer.js:
import { SET_CURRENT_USER } from "../actions/auth.actions";
import isEmpty from "../../assets/common/isEmpty";
export default function (state, action) {
switch (action.type) {
case SET_CURRENT_USER:
return {
...state,
isAuthenticated: !isEmpty(action.payload),
user: action.payload,
userProfile: action.userProfile
};
default:
return state;
}
}
Auth.js(来自商店):
import React, { useReducer, useEffect, useState } from 'react';
import jwt_decode from 'jwt-decode';
import AsyncStorage from '@react-native-async-storage/async-storage';
import authReducer from '../reducers/auth.reducer';
import { setCurrentUser } from '../actions/auth.actions';
import AuthGlobal from './AuthGlobal';
const Auth = (props) => {
const [stateUser, dispatch] = useReducer(authReducer, {
isAuthenticated: null,
user: {},
});
const [showChild, setShowChild] = useState(false);
useEffect(() => {
setShowChild(true);
if (AsyncStorage.jwt) {
const decoded = AsyncStorage.jwt ? AsyncStorage.jwt : '';
if (setShowChild) {
dispatch(setCurrentUser(jwt_decode(decoded)));
}
}
return () => setShowChild(false);
}, []);
if (!showChild) {
return null;
} else {
return (
<AuthGlobal.Provider
value={{
stateUser,
dispatch,
}}
>
{props.children}
</AuthGlobal.Provider>
);
}
};
export default Auth;
来自 Expo 的堆栈跟踪
[Unhandled promise rejection: Error: Request failed with status code 503]
at node_modules/axios/lib/core/createError.js:15:17 in createError
at node_modules/axios/lib/core/settle.js:16:9 in settle
at node_modules/axios/lib/adapters/xhr.js:57:6 in onloadend
at node_modules/react-native/Libraries/Network/XMLHttpRequest.js:614:6 in setReadyState
at node_modules/react-native/Libraries/Network/XMLHttpRequest.js:396:6 in __didCompleteResponse
at node_modules/react-native/Libraries/vendor/emitter/_EventEmitter.js:135:10 in EventEmitter#emit
Heroku 日志:
2022-05-21T15:59:08.640508+00:00 heroku[router]: at=error code=H13 desc="Connection closed without response" method=GET path="/api/v1/users/undefined" host=<HIDDEN> request_id=9f36d0a5-6b50-4b33-89f3-1d9f16302954 fwd="<HIDDEN>" dyno=web.1 connect=0ms service=10ms status=503 bytes=0 protocol=http
从表面上看,这似乎是某种问题,因为我们从 asyncstorage 中撤销了 jwt 令牌,Heroku 正在等待响应并且不会得到响应
错误的请求 GET /api/v1/users/undefined
可能是由您的代码的以下部分引起的:
if (context.stateUser.isAuthenticated === false ||
context.stateUser.isAuthenticated === null) {
props.navigation.navigate('Login');
}
// "else" missing here?
AsyncStorage.getItem('jwt').then((res) => {
axios.get(`${baseURL}users/${context.stateUser.user.userId}`, ...
因为 axios.get
语句即使没有用户通过身份验证也会执行。
我正在构建一个带有用户身份验证的 React Native 应用程序。每次我 'logout' 一个用户(使用 JWT)。我注销了,然后在 x 秒后抛出 503。和 Heroku 崩溃,我每次都必须手动重启 Heroku Dyno,(或等待 10 分钟)
包:
后端:
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.2.6",
"morgan": "^1.10.0",
"multer": "^1.4.4",
"nodemon": "^2.0.15"
}
前端:
"dependencies": {
"@react-native-async-storage/async-storage": "~1.15.0",
"@react-navigation/bottom-tabs": "^6.2.0",
"@react-navigation/material-top-tabs": "^6.2.1",
"@react-navigation/native": "^6.0.8",
"@react-navigation/stack": "^6.2.1",
"axios": "^0.26.1",
"expo": "~44.0.0",
"expo-status-bar": "~1.2.0",
"jwt-decode": "^3.1.2",
"native-base": "^2.13.14",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3",
"react-native-gesture-handler": "~2.1.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-reanimated": "~2.3.1",
"react-native-safe-area-context": "3.3.2",
"react-native-screens": "~3.10.1",
"react-native-swipe-list-view": "^3.2.9",
"react-native-swiper": "^1.6.0",
"react-native-tab-view": "^3.1.1",
"react-native-toast-message": "^2.1.5",
"react-native-vector-icons": "^9.1.0",
"react-native-web": "0.17.1",
"react-redux": "^7.2.8",
"redux": "^4.1.2",
"redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.1",
"undefined": "@react-native/community/masked-view"
},
"devDependencies": {
"@babel/core": "^7.12.9"
},
"private": true
}
后端App.js
const express = require('express');
const app = express();
require('dotenv/config');
const morgan = require('morgan');
const mongoose = require('mongoose');
const cors = require('cors');
const errorHandler = require('./helpers/error-handler');
//cors
app.use(cors());
app.options('*', cors());
const productsRouter = require('./routers/products');
const categoriesRouter = require('./routers/categories');
const ordersRouter = require('./routers/orders');
const usersRouter = require('./routers/users');
const authJwt = require('./helpers/jwt');
const api = process.env.API_URL;
// MiddleWare
app.use(express.json());
app.use(morgan('tiny'));
app.use(authJwt());
app.use('/public/uploads', express.static(__dirname + '/public/uploads'));
app.use(errorHandler);
// Routers
app.use(`${api}/categories`, categoriesRouter);
app.use(`${api}/orders`, ordersRouter);
app.use(`${api}/products`, productsRouter);
app.use(`${api}/users`, usersRouter);
mongoose
.connect(process.env.CONNECTION_STRING)
.then(() => {
console.log('database connection is ready');
})
.catch((err) => {
console.log(err);
});
// Development
// app.listen(3000, () => {
// console.log('JamComMobile BE running on http://localhost:3000');
// });
// Production
let server = app.listen(process.env.PORT || 4000, function () {
var port = server.address().port;
console.log('Express is working on port ' + port)
})
server.on('clientError', (err, socket) => {
console.error(err);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
后端用户模型
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
passwordHash: {
type: String,
required: true,
},
phone: {
type: String,
required: true,
},
isAdmin: {
type: Boolean,
default: false,
},
street: {
type: String,
default: ''
},
apartment: {
type: String,
default: ''
},
zip :{
type: String,
default: ''
},
city: {
type: String,
default: ''
},
country: {
type: String,
default: ''
}
});
userSchema.virtual('id').get(function () {
return this._id.toHexString();
});
userSchema.set('toJSON', {
virtuals: true,
});
exports.User = mongoose.model('User', userSchema);
exports.userSchema = userSchema;
后端jwt.js
const expressJwt = require('express-jwt');
function authJwt() {
const secret = process.env.secret;
const api = process.env.API_URL;
return expressJwt({
secret,
algorithms: ['HS256'],
isRevoked: isRevoked,
}).unless({
path: [
{ url: /\/public\/uploads(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/products(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/categories(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/orders(.*)/, methods: ['GET', 'OPTIONS', 'POST'] },
// { url: /\/api\/v1\/users(.*)/, methods: ['GET', 'OPTIONS'] },
{ url: /\/api\/v1\/users\/.*/, methods: ['GET'] },
`${api}/users/login`,
`${api}/users/register`,
],
});
}
//TODO: need to clean this up and add additional roles
//this rejects the api call if user is not admin
// async function isRevoked(req, payload, done) {
// if (!payload.isAdmin) {
// done(null, true);
// }
// done();
// }
async function isRevoked(req, token) { // token now contains payload data
if(!token.payload.isAdmin) {
return true // if the isAdmin flag in payload is false, then we reject the token
}
return false ;
}
module.exports = authJwt;
前端UserProfile.js(使用注销方法)
import React, { useContext, useState, useCallback } from 'react';
import { View, Text, ScrollView, StyleSheet, Button } from 'react-native';
import { Container } from 'native-base';
import { useFocusEffect } from '@react-navigation/native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import axios from 'axios';
import baseURL from '../../assets/common/baseUrl';
import AuthGlobal from '../../Context/store/AuthGlobal';
import { logoutUser } from '../../Context/actions/auth.actions';
const UserProfile = (props) => {
const context = useContext(AuthGlobal);
const [userProfile, setUserProfile] = useState();
useFocusEffect(
useCallback(() => {
if (
context.stateUser.isAuthenticated === false ||
context.stateUser.isAuthenticated === null
) {
props.navigation.navigate('Login');
}
AsyncStorage.getItem('jwt')
.then((res) => {
axios
.get(`${baseURL}users/${context.stateUser.user.userId}`, {
headers: { Authorization: `Bearer ${res}` },
})
.then((user) => setUserProfile(user.data));
})
.catch((error) => console.log(error));
return () => {
setUserProfile();
};
}, [context.stateUser.isAuthenticated]),
);
return (
<Container style={styles.container}>
<ScrollView contentContainerStyle={styles.subContainer}>
<Text style={{ fontSize: 30 }}>
{userProfile ? userProfile.name : ''}
</Text>
<View style={{ marginTop: 20 }}>
<Text style={{ margin: 10 }}>
Email: {userProfile ? userProfile.email : ''}
</Text>
<Text style={{ margin: 10 }}>
Phone: {userProfile ? userProfile.phone : ''}
</Text>
</View>
<View style={{ marginTop: 80 }}>
<Button
title={'Sign Out'}
onPress={() => [
logoutUser(context.dispatch),
AsyncStorage.removeItem('jwt').catch(
(error) => console.log(error)
),
]}
/>
</View>
</ScrollView>
</Container>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
},
subContainer: {
alignItems: 'center',
marginTop: 60,
},
});
export default UserProfile;
React 上下文 API AuthActions.js
import jwt_decode from 'jwt-decode';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Toast from 'react-native-toast-message';
import baseURL from '../../assets/common/baseUrl';
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
export const loginUser = (user, dispatch) => {
fetch(`${baseURL}users/login`, {
method: 'POST',
body: JSON.stringify(user),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((data) => {
if (data) {
const token = data.token;
AsyncStorage.setItem('jwt', token);
const decoded = jwt_decode(token);
dispatch(setCurrentUser(decoded, user));
} else {
logoutUser(dispatch);
}
})
.catch((err) => {
Toast.show({
topOffset: 60,
type: 'error',
text1: 'Please provide correct credentials in order to login',
});
logoutUser(dispatch);
});
};
export const getUserProfile = (id) => {
fetch(`${baseURL}users/${id}`, {
method: 'GET',
body: JSON.stringify(user),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((data) => console.log(data));
};
export const logoutUser = (dispatch) => {
AsyncStorage.removeItem('jwt');
dispatch(setCurrentUser({}));
};
export const setCurrentUser = (decoded, user) => {
return {
type: SET_CURRENT_USER,
payload: decoded,
userProfile: user,
};
};
AuthReducer.js:
import { SET_CURRENT_USER } from "../actions/auth.actions";
import isEmpty from "../../assets/common/isEmpty";
export default function (state, action) {
switch (action.type) {
case SET_CURRENT_USER:
return {
...state,
isAuthenticated: !isEmpty(action.payload),
user: action.payload,
userProfile: action.userProfile
};
default:
return state;
}
}
Auth.js(来自商店):
import React, { useReducer, useEffect, useState } from 'react';
import jwt_decode from 'jwt-decode';
import AsyncStorage from '@react-native-async-storage/async-storage';
import authReducer from '../reducers/auth.reducer';
import { setCurrentUser } from '../actions/auth.actions';
import AuthGlobal from './AuthGlobal';
const Auth = (props) => {
const [stateUser, dispatch] = useReducer(authReducer, {
isAuthenticated: null,
user: {},
});
const [showChild, setShowChild] = useState(false);
useEffect(() => {
setShowChild(true);
if (AsyncStorage.jwt) {
const decoded = AsyncStorage.jwt ? AsyncStorage.jwt : '';
if (setShowChild) {
dispatch(setCurrentUser(jwt_decode(decoded)));
}
}
return () => setShowChild(false);
}, []);
if (!showChild) {
return null;
} else {
return (
<AuthGlobal.Provider
value={{
stateUser,
dispatch,
}}
>
{props.children}
</AuthGlobal.Provider>
);
}
};
export default Auth;
来自 Expo 的堆栈跟踪
[Unhandled promise rejection: Error: Request failed with status code 503]
at node_modules/axios/lib/core/createError.js:15:17 in createError
at node_modules/axios/lib/core/settle.js:16:9 in settle
at node_modules/axios/lib/adapters/xhr.js:57:6 in onloadend
at node_modules/react-native/Libraries/Network/XMLHttpRequest.js:614:6 in setReadyState
at node_modules/react-native/Libraries/Network/XMLHttpRequest.js:396:6 in __didCompleteResponse
at node_modules/react-native/Libraries/vendor/emitter/_EventEmitter.js:135:10 in EventEmitter#emit
Heroku 日志:
2022-05-21T15:59:08.640508+00:00 heroku[router]: at=error code=H13 desc="Connection closed without response" method=GET path="/api/v1/users/undefined" host=<HIDDEN> request_id=9f36d0a5-6b50-4b33-89f3-1d9f16302954 fwd="<HIDDEN>" dyno=web.1 connect=0ms service=10ms status=503 bytes=0 protocol=http
从表面上看,这似乎是某种问题,因为我们从 asyncstorage 中撤销了 jwt 令牌,Heroku 正在等待响应并且不会得到响应
错误的请求 GET /api/v1/users/undefined
可能是由您的代码的以下部分引起的:
if (context.stateUser.isAuthenticated === false ||
context.stateUser.isAuthenticated === null) {
props.navigation.navigate('Login');
}
// "else" missing here?
AsyncStorage.getItem('jwt').then((res) => {
axios.get(`${baseURL}users/${context.stateUser.user.userId}`, ...
因为 axios.get
语句即使没有用户通过身份验证也会执行。