我有一个 Nuxt JS 应用程序设置来使用Nuxt Auth。这在本地运行良好。
具体来说,我正在生成一封发送给用户的电子邮件,其中包含重置表单密码的链接
http://localhost:3000/reset-password/ca62c3554c8058c9ddf11b709fc451405ffa99f4b22a88d84e087f5b40fb6d1f
当他们单击它时 - 它被解析 JWT 的 nuxt 路由拾取。在本地,我使用 nuxt start 服务它 - 我相信它从 dist 目录服务,因此应该是静态服务的一个很好的测试
当我将它部署到运行 Ubuntu 和 Plesk 以及 Nginx 和 Apache 的远程 lightail 服务器时,我使用 nuxt generate 部署它并将生成的 dist 目录的内容复制到 httpdocs 目录。当遵循相同的工作流程并且用户单击链接时,它不会被 nuxt 生成的静态 html 文件之一捕获,并且我得到 404。所有其他 nuxt 路由正在生成到文件中。我错过了什么?
nuxt.config.js
export default {
target: 'static',
loading: {
color: '#3700b3',
height: '5px',
},
env: {
apiUrl: process.env.NODE_ENV === 'production' ? process.env.PLATFORM_API_URL : 'http://localhost:8000',
mainUrl: process.env.NODE_ENV === 'production' ? process.env.PLATFORM_URL : 'http://localhost:3000',
googleSiteKey: process.env.RECAPTCHA_SITE_KEY || '',
},
ssr: false,
head: {
titleTemplate: `%s - ${process.env.PLATFORM_NAME || 'Some platform name'}`,
title: process.env.PLATFORM_NAME || 'Some platform name',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' },
{ hid: 'description', name: 'description', content: 'Virtua Centre' },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
script: [
{
src: 'https://platform.twitter.com/widgets.js',
},
{
src: 'https://js.stripe.com/v3',
},
],
},
plugins: [
{
src: '@/plugins/vue-page-transition.js',
},
{
src: '@/plugins/plateform-detector.js',
},
{ src: '~/plugins/TiptapVuetify', mode: 'client' },
{ src: '@/plugins/filters.js' },
{ src: '~/plugins/i18n.js' },
{ src: '~/plugins/locales.js' },
],
components: true,
buildModules: [
'@nuxtjs/eslint-module',
'@nuxtjs/stylelint-module',
['@nuxtjs/vuetify'],
'@nuxtjs/date-fns',
],
modules: [
'nuxt-i18n',
'@nuxtjs/axios',
'@nuxtjs/auth-next',
['v-currency-field/nuxt-treeshaking'],
'vue-currency-filter/nuxt',
'vuetify-dialog/nuxt',
],
i18n: {
strategy: 'no_prefix',
locales: [
{
code: 'en',
name: 'English',
file: 'en-US.js',
flag: '/flag-icon/flags/1x1/us.svg',
},
{
code: 'kk',
name: 'Kazakh',
file: 'en-KK.js',
flag: '/flag-icon/flags/1x1/kz.svg',
},
{
code: 'ru',
name: 'Russian',
file: 'en-RU.js',
flag: '/flag-icon/flags/1x1/ru.svg',
},
],
lazy: true,
langDir: 'lang',
defaultLocale: 'en',
vueI18n: {
fallbackLocale: 'en',
},
},
axios: {
credentials: true,
baseURL: process.env.NODE_ENV === 'production' ? process.env.PLATFORM_API_URL : 'http://localhost:8000',
},
auth: {
redirect: {
login: '/login',
logout: false,
callback: '/',
home: false,
},
strategies: {
local: {
token: {
property: 'data.access_token',
maxAge: 36000,
},
user: {
property: 'data',
},
endpoints: {
login: { url: '/auth/login', method: 'post' },
logout: { url: '/logout', method: 'post' },
user: { url: '/me', method: 'get' },
},
},
},
},
vue: {
config: {
productionTip: false,
devtools: true,
},
},
vuetify: {
theme: {
themes: {
light: {
primary: '#4F91FF',
secondary: '#00109c',
success: '#00B485',
lsmbutton: '#FFBF42',
error: '#F85032',
},
},
},
},
build: {
extractCSS: true,
transpile: ['vuetify/lib', 'tiptap-vuetify', 'vee-validate/dist/rules'],
babel: {
plugins: [['@babel/plugin-proposal-private-methods', { loose: true }]],
},
extend(config, ctx) {
config.module.rules.push({
test: /\.(ogg|mp3|wav|mpe?g)$/i,
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
},
})
},
splitChunks: {
layouts: true,
},
},
}
package.json
的脚本部分
"scripts": {
"dev": "nuxt --hostname 127.0.0.1 --port 3000",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
"lintfix": "eslint --fix --ext .vue --ignore-path .gitignore .",
"lint:style": "stylelint **/*.{vue,css} --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"test": "jest"
},
我正在使用 npm
阅读周围我可以看到处理动态路由的标准解决方案是更新 nuxt.config.js generate.routes 部分中的配置。如本文所述
这似乎可以通过在生成时从服务器获取所有值来实现。我认为这不适用于身份验证令牌,因为用户可以随时注册 - 特别是在运行 nuxt generate 之后。
重置密码功能
- 页面
- 重设密码
- 索引.vue
- _token.vue
索引.vue
<template>
<div v-show="!loading">
<section
class="login-bg"
:class="[$vuetify.breakpoint.mdAndUp ? 'xl-full-width' : 'sm-full-width']"
>
<v-row class="justify-center-custom">
<div
class="cont mb-5"
:class="[$vuetify.breakpoint.smAndUp ? 'small-width' : 'full-width']"
>
<div class="form-right">
<div class="card-body">
<h3 class="text-center">
<!-- TODO: translate -->
<strong>Forgot Password?</strong>
</h3>
<form v-if="!message" @submit.prevent="submit()">
<p
class="text-center"
style="margin-top: 10px; margin-bottom: 10px"
>
Enter the email ID you used when you joined and we will send
you temporary password
</p>
<br />
<div class="form-group mb-50">
<label class="text-bold-600">{{ 'E-Mail Address' }}</label>
<input
id="email"
v-model="email"
type="email"
class="form-control"
:class="{ 'is-invalid': error.email }"
name="email"
required
autocomplete="off"
autofocus
/>
<span
v-if="error.email"
class="invalid-feedback"
role="alert"
>
<strong>{{ error.email }}</strong>
</span>
</div>
<button
type="submit"
class="btn ml-0 btn-login btn-primary w-100"
>
<span
v-if="formSubmitting"
class="spinner-border spinner-border-sm mr-1"
role="status"
aria-hidden="true"
></span>
{{ 'Send Password Reset Link' }}
<i class="fa fa-arrow-right"></i>
</button>
</form>
<div v-else>
<p
class="text-center text-success"
style="margin-top: 10px; margin-bottom: 10px"
>
{{ message }}
</p>
</div>
</div>
</div>
</div>
</v-row>
</section>
</div>
</template>
<script>
import AssetLoader from '@/mixins/AssetLoader'
export default {
layout: 'landing',
mixins: [AssetLoader],
data() {
return {
error: {
email: false,
},
email: null,
loading: false,
formSubmitting: false,
message: '',
}
},
async beforeDestroy() {
await this.unloadCSS(
'https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css'
)
await this.unloadCSS(
'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,800;1,900&display=swap'
)
await this.unloadCSS('/css/mdb.min.css')
await this.unloadCSS('/css/style.css')
await this.unloadCSS('/css/landing-school.css')
await this.unloadCSS('/css/landing-school-options.css')
await this.unloadCSS(
'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'
)
await this.unloadScript('/js/jquery-3.4.1.min.js')
await this.unloadScript('/js/popper.min.js')
await this.unloadScript('/js/bootstrap.min.js')
await this.unloadScript('/js/popup.js')
await this.unloadScript('/js/owl.carousel.js')
await this.unloadScript('/js/jquery.nivo.slider.js')
await this.unloadScript('/js/landing-school.js')
},
async mounted() {
this.loading = true
await this.loadCSS([
'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css',
'https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css',
'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,800;1,900&display=swap',
'/css/mdb.min.css',
'/css/landing-school.css',
'/css/landing-school-options.css',
])
await this.loadJS([
'/js/jquery-3.4.1.min.js',
'/js/popper.min.js',
'/js/bootstrap.min.js',
'/js/popup.js',
'/js/owl.carousel.js',
'/js/jquery.nivo.slider.js',
// '/js/landing-school.js'
])
this.loading = false
},
methods: {
async submit() {
this.formSubmitting = true
this.error.email = ''
try {
const { data } = await this.$axios.post('/auth/forgot-password', {
email: this.email,
})
this.message = data.status
this.email = ''
this.formSubmitting = false
} catch (error) {
this.formSubmitting = false
if (error?.response?.data?.errorsArray?.length) {
this.error.email = error?.response?.data?.errorsArray[0]
} else if (error?.response?.data?.email) {
this.error.email = error?.response?.data?.email
} else {
this.error.email = error.message
}
}
},
},
}
</script>
<style scoped>
.navbar {
display: none !important;
}
#btn-amazon {
background: #f90 !important;
border-color: #f90 !important;
color: #fff !important;
}
#btn-apple {
background: #7e878b !important;
border-color: #7e878b !important;
color: #fff !important;
}
#btn-twitter {
background: #32def4 !important;
border-color: #32def4 !important;
color: #fff !important;
}
.btn-login {
background: -webkit-linear-gradient(45deg, #303f9f, #7b1fa2);
background: linear-gradient(45deg, #303f9f, #7b1fa2);
box-shadow: 3px 3px 20px 0 rgba(123, 31, 162, 0.5);
}
.form-left {
padding: 114px 10px;
}
.cont {
border-radius: 10px;
display: block;
}
.cont.full-width {
width: 90%;
}
.cont.small-width {
width: 600px;
}
.row.justify-center-custom {
justify-content: center;
}
.login-bg.xl-full-width {
height: 100vh;
}
.login-bg.sm-full-width {
height: 100%;
}
</style>
_token.vue
<template>
<div v-show="!loading">
<section
class="login-bg"
:class="[$vuetify.breakpoint.mdAndUp ? 'xl-full-width' : 'sm-full-width']"
>
<v-row class="justify-center-custom">
<div
class="cont mb-5"
:class="[$vuetify.breakpoint.smAndUp ? 'small-width' : 'full-width']"
>
<div class="form-right">
<div class="card-body">
<h3 class="text-center">
<!-- TODO: translate -->
<strong>Reset Password</strong>
</h3>
<form v-if="!message" @submit.prevent="submit()">
<p
class="text-center"
style="margin-top: 10px; margin-bottom: 10px"
>
Enter the email ID you used when you joined and we will send
you temporary password
</p>
<br />
<div v-if="errors.length" class="card px-3 py-3 mb-3">
<ol class="text-danger mb-0" style="list-style-type: none">
<li v-for="(error, index) in errors" :key="index">
<strong>{{ error }}</strong>
</li>
</ol>
</div>
<div class="form-group mb-50">
<label class="text-bold-600">{{ 'E-Mail Address' }}</label>
<input
v-model="email"
type="email"
class="form-control"
name="email"
autocomplete="off"
autofocus
/>
</div>
<div class="form-group mb-50">
<label class="text-bold-600">{{ 'Password' }}</label>
<input
v-model="password"
type="password"
class="form-control"
autocomplete="off"
autofocus
/>
</div>
<div class="form-group mb-50">
<label class="text-bold-600">{{ 'Confirm Password' }}</label>
<input
v-model="password_confirmation"
type="password"
class="form-control"
autocomplete="off"
autofocus
/>
</div>
<button
type="submit"
class="btn ml-0 btn-login btn-primary w-100"
>
<span
v-if="formSubmitting"
class="spinner-border spinner-border-sm mr-1"
role="status"
aria-hidden="true"
></span>
{{ 'Reset Password' }}
<i class="fa fa-arrow-right"></i>
</button>
</form>
<div v-else>
<p
class="text-center text-success"
style="margin-top: 10px; margin-bottom: 10px"
>
{{ message }}
</p>
<button
type="button"
class="btn ml-0 btn-login btn-primary w-100"
@click="$router.push('/login')"
>
{{ $t('login_now') }}
<i class="fa fa-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
</v-row>
</section>
</div>
</template>
<script>
import AssetLoader from '@/mixins/AssetLoader'
export default {
layout: 'landing',
mixins: [AssetLoader],
data() {
return {
errors: [],
token: null,
email: null,
password: null,
password_confirmation: null,
loading: false,
formSubmitting: false,
message: '',
}
},
created() {
if (this.$route.params.token) {
this.token = this.$route.params.token
}
},
async beforeDestroy() {
await this.unloadCSS(
'https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css'
)
await this.unloadCSS(
'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,800;1,900&display=swap'
)
await this.unloadCSS('/css/mdb.min.css')
await this.unloadCSS('/css/style.css')
await this.unloadCSS('/css/landing-school.css')
await this.unloadCSS('/css/landing-school-options.css')
await this.unloadCSS(
'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'
)
await this.unloadScript('/js/jquery-3.4.1.min.js')
await this.unloadScript('/js/popper.min.js')
await this.unloadScript('/js/bootstrap.min.js')
await this.unloadScript('/js/popup.js')
await this.unloadScript('/js/owl.carousel.js')
await this.unloadScript('/js/jquery.nivo.slider.js')
await this.unloadScript('/js/landing-school.js')
},
async mounted() {
this.loading = true
await this.loadCSS([
'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css',
'https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css',
'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,800;1,900&display=swap',
'/css/mdb.min.css',
'/css/landing-school.css',
'/css/landing-school-options.css',
])
await this.loadJS([
'/js/jquery-3.4.1.min.js',
'/js/popper.min.js',
'/js/bootstrap.min.js',
'/js/popup.js',
'/js/owl.carousel.js',
'/js/jquery.nivo.slider.js',
// '/js/landing-school.js'
])
this.loading = false
},
methods: {
async submit() {
this.formSubmitting = true
this.errorsl = []
try {
const { data } = await this.$axios.post('/auth/reset-password', {
email: this.email,
token: this.token,
password_confirmation: this.password_confirmation,
password: this.password,
})
this.message = data.status
this.email = ''
this.formSubmitting = false
} catch (error) {
this.formSubmitting = false
if (error?.response?.data?.errorsArray?.length) {
this.errors = error?.response?.data?.errorsArray
} else if (error?.response?.data?.email) {
this.errors = [error?.response?.data?.email]
}
}
},
},
}
</script>
<style scoped>
.navbar {
display: none !important;
}
#btn-amazon {
background: #f90 !important;
border-color: #f90 !important;
color: #fff !important;
}
#btn-apple {
background: #7e878b !important;
border-color: #7e878b !important;
color: #fff !important;
}
#btn-twitter {
background: #32def4 !important;
border-color: #32def4 !important;
color: #fff !important;
}
.btn-login {
background: -webkit-linear-gradient(45deg, #303f9f, #7b1fa2);
background: linear-gradient(45deg, #303f9f, #7b1fa2);
box-shadow: 3px 3px 20px 0 rgba(123, 31, 162, 0.5);
}
.form-left {
padding: 114px 10px;
}
.cont {
border-radius: 10px;
display: block;
}
.cont.full-width {
width: 90%;
}
.cont.small-width {
width: 600px;
}
.row.justify-center-custom {
justify-content: center;
}
.login-bg.xl-full-width {
height: 100vh;
}
.login-bg.sm-full-width {
height: 100%;
}
</style>
最终,在 Plesk Apache 上进行这项工作的答案是将 .htaccess 文件添加到与 index.html 相同的目录中。内容为:-
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
在这里解释