1

我有一个 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>

在这里解释

4

2 回答 2

1

Netlify 上的解决方案是在构建中添加一些特定的配置以进行重定向。在要部署的 repo 分支的根目录中创建 netlify.toml。

Netlify.toml 包含:-

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

以此为导

我将其解读为 - 如果 nuxt 生成了一个 html 文件,它将为它提供服务,并且您的路线会很好。但是如果它没有生成一个与路由完全匹配的文件,那么您需要调用应用程序的入口点来初始化它并让您需要的路由可用。

于 2021-07-10T11:50:38.453 回答
0

不确定您的 Plesk + Nginx + Apache 特定配置,但将其托管在 Netlify 上仍然是最简单和最快的解决方案。

target: static另外,ssr: true如果可行的话,我会去。

于 2021-07-12T13:15:51.863 回答