Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementation Guide for JWT Authentication, Vant UI Integration and Common Interactive Features in Vue + Node.js Mobile Fullstack Projects

Tech 2

Backend JWT Authentication Setup

Install jsonwebtokan dependency first:

npm install jsonwebtoken --save

Generate JWT on successful login

Add login endpoint handler in user route file:

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

router.post('/api/user/login', async (req, res) => {
  const { userTel, loginPwd } = req.body;
  const matchedUsers = await sql.query(UserModel, { telephone: userTel }, { __v: 0 });
  if (matchedUsers.length === 0) {
    return res.json({
      code: '20001',
      msg: 'User not registered'
    });
  }
  const storedPwd = matchedUsers[0].password;
  const pwdValid = bcrypt.compareSync(loginPwd, storedPwd);
  if (!pwdValid) {
    return res.json({
      code: '20002',
      msg: 'Incorrect password'
    });
  }
  const userId = matchedUsers[0].uid;
  const token = jwt.sign({ userId }, 'fullstack_jwt_secret_2024', {
    expiresIn: 7200 // Token valid for 2 hours
  });
  res.json({
    code: '20000',
    msg: 'Login successful',
    authToken: token
  });
});

Global JWT validation middleware

Add the following middleware in app.js before route registration to protect private interfaces:

const jwt = require('jsonwebtoken');

app.use((req, res, next) => {
  // Exclude public interfaces from authentication
  const publicPaths = ['/api/user/login', '/api/user/register', '/api/banner'];
  if (!publicPaths.includes(req.path)) {
    // Fetch token from header, query or request body
    const token = req.headers.authtoken || req.query.authToken || req.body.authToken;
    if (!token) {
      return res.json({
        code: '20003',
        msg: 'No valid authentication credentials found'
      });
    }
    jwt.verify(token, 'fullstack_jwt_secret_2024', (err, payload) => {
      if (err) {
        return res.json({
          code: '20003',
          msg: 'Authentication expired, please login again'
        });
      }
      req.userInfo = payload;
      next();
    });
  } else {
    next();
  }
});

Frontend Login Module Implementation

Login page development

Create views/login/index.vue file:

<template>
  <div class="login-wrapper">
    <div class="page-header">Account Login</div>
    <div class="form-content">
      <input type="tel" placeholder="Please enter mobile number" v-model="phoneNum" />
      <p class="error-tip">{{ phoneErrMsg }}</p>
      <input type="password" placeholder="Please enter password" v-model="loginPwd" />
      <p class="error-tip">{{ pwdErrMsg }}</p>
      <button class="submit-btn" @click="handleLogin">Login Now</button>
    </div>
  </div>
</template>
<script>
import axios from 'axios';
export default {
  data() {
    return {
      phoneNum: '',
      loginPwd: '',
      submitErr: ''
    }
  },
  computed: {
    phoneErrMsg() {
      if (!this.phoneNum) return '';
      return /^1[3-9]\d{9}$/.test(this.phoneNum) ? '' : 'Mobile number format is incorrect';
    },
    pwdErrMsg() {
      if (!this.loginPwd) return '';
      return this.loginPwd.length >=6 ? '' : 'Password must be at least 6 characters long';
    }
  },
  methods: {
    async handleLogin() {
      if (this.phoneErrMsg || !this.phoneNum) {
        this.submitErr = 'Please enter valid mobile number';
        return;
      }
      if (this.pwdErrMsg || !this.loginPwd) {
        this.submitErr = 'Please enter valid password';
        return;
      }
      const res = await axios.post('/api/user/login', {
        userTel: this.phoneNum,
        loginPwd: this.loginPwd
      });
      if (res.data.code === '20001') {
        this.submitErr = 'Account not registered, please register first';
      } else if (res.data.code === '20002') {
        this.submitErr = 'Password is incorrect, please try again';
      } else if (res.data.code === '20000') {
        this.submitErr = '';
        localStorage.setItem('auth_token', res.data.authToken);
        this.$router.push('/home');
      }
    }
  }
}
</script>
<style scoped lang="scss">
.login-wrapper {
  width: 100%;
  height: 100vh;
  background: #fff;
}
.page-header {
  height: 44px;
  line-height: 44px;
  text-align: center;
  font-size: 16px;
  font-weight: 500;
  border-bottom: 1px solid #eee;
}
.form-content {
  padding: 20px 15px;
}
input {
  width: 100%;
  height: 45px;
  line-height: 45px;
  border: none;
  border-bottom: 1px solid #eee;
  text-indent: 10px;
  margin-bottom: 5px;
  font-size: 14px;
}
.error-tip {
  height: 18px;
  line-height: 18px;
  color: #ff4d4f;
  font-size: 12px;
  text-align: center;
}
.submit-btn {
  width: 100%;
  height: 45px;
  line-height: 45px;
  border: none;
  border-radius: 4px;
  background: #ff4d4f;
  color: #fff;
  font-size: 16px;
  margin-top: 20px;
}
</style>

Login route configuration

Add login route entry in router/index.js:

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login/index.vue')
  }
]

Authentication for Private Pages

Homepage access control

Adjust homepage template to show content only after successful authentication:

<template>
  <div class="home-page">
    <div class="page-content">
      <ProductList v-if="isLoggedIn" :productList="productList" />
      <div v-else class="auth-tip">
        <p>Please login to view product content</p>
        <router-link to="/login" class="login-link">Go to login</router-link>
      </div>
    </div>
  </div>
</template>
<script>
import axios from 'axios';
import ProductList from '@/components/ProductList.vue';
export default {
  components: { ProductList },
  data() {
    return {
      productList: [],
      isLoggedIn: false
    }
  },
  async created() {
    const token = localStorage.getItem('auth_token');
    const res = await axios.get(`/api/product?authToken=${token}`);
    if (res.data.code === '20003') {
      this.isLoggedIn = false;
    } else {
      this.isLoggedIn = true;
      this.productList = res.data.data;
    }
  }
}
</script>

Product detail page access control

Adjust detail page logic to verify authentication before loading data:

<template>
  <div class="detail-page">
    <div class="page-content" v-if="isLoggedIn">
      <img :src="productCover" alt="product cover" class="prod-img" />
      <h2 class="prod-name">{{ productName }}</h2>
      <p class="prod-price">¥ {{ productPrice }}</p>
      <div class="prod-desc">{{ productDesc }}</div>
      <div class="comment-section">
        <div class="comment-item" v-for="item in commentList" :key="item.commentId">
          <h4>{{ item.nickname }} <span class="rating">{{ item.rate }} stars</span></h4>
          <p>{{ item.content }}</p>
        </div>
      </div>
    </div>
    <div v-else class="auth-tip">
      <p>Please login to view product details</p>
      <router-link to="/login" class="login-link">Go to login</router-link>
    </div>
  </div>
</template>
<script>
import axios from 'axios';
export default {
  data() {
    return {
      isLoggedIn: false,
      productId: '',
      productName: '',
      productCover: '',
      productPrice: '',
      productDesc: '',
      commentList: []
    }
  },
  async created() {
    this.productId = this.$route.query.pid;
    const token = localStorage.getItem('auth_token');
    const [prodRes, commentRes] = await Promise.all([
      axios.get(`/api/product/detail?pid=${this.productId}&authToken=${token}`),
      axios.get(`/api/comment/list?pid=${this.productId}&authToken=${token}`)
    ]);
    if (prodRes.data.code === '20003' || commentRes.data.code === '20003') {
      this.isLoggedIn = false;
    } else {
      this.isLoggedIn = true;
      const prodInfo = prodRes.data.data;
      this.productName = prodInfo.name;
      this.productCover = prodInfo.cover;
      this.productPrice = prodInfo.price;
      this.productDesc = prodInfo.desc;
      this.commentList = commentRes.data.data;
    }
  }
}
</script>

Vant UI Integration

Vant is a lightweight mobile UI component library suitable for Vue-based mobile projects.

Install dependencies

npm install vant --save
npm install babel-plugin-import --save-dev

On-demand import configuration

Modify babel.config.js to enable on-demand component loading:

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
}

Note: Restart the local development server after modifying Babel configuraton to take effect.

Implement Common Interactive Features

Homepage carousel

  1. Register Swipe component in entry file or homepage:
import Vue from 'vue';
import { Swipe, SwipeItem } from 'vant';
Vue.use(Swipe).use(SwipeItem);
  1. Load banner data (public interface, no authentication required):
async created() {
  const bannerRes = await axios.get('/api/banner');
  this.bannerList = bannerRes.data.data;
  // Other request logic
}
  1. Render carousel in template:
<van-swipe autoplay="3000" indicator-color="#fff">
  <van-swipe-item v-for="item in bannerList" :key="item.bid">
    <img :src="item.imgUrl" alt="banner" class="banner-img" />
  </van-swipe-item>
</van-swipe>

Product detail page bottom action bar

Import and register GoodsAction component, add to detail page bottom:

import { GoodsAction, GoodsActionIcon, GoodsActionButton } from 'vant';
Vue.use(GoodsAction).use(GoodsActionIcon).use(GoodsActionButton);
<van-goods-action>
  <van-goods-action-icon icon="chat-o" text="Customer Service" />
  <van-goods-action-icon icon="cart-o" text="Cart" />
  <van-goods-action-button type="warning" text="Add to Cart" />
  <van-goods-action-button type="danger" text="Buy Now" />
</van-goods-action>

City selector page

  1. Add route for city selector:
{
  path: '/city-select',
  name: 'CitySelect',
  component: () => import('@/views/city/index.vue')
}
  1. Implement city selector with Vant IndexBar:
<template>
  <div class="city-page">
    <div class="page-header">Select City</div>
    <div class="page-content">
      <van-index-bar>
        <van-index-anchor v-for="(group, idx) in cityGroupList" :key="idx" :index="group.initial">
          <van-cell v-for="city in group.cityList" :key="city.cid" :title="city.cityName" @click="selectCity(city)" />
        </van-index-anchor>
      </van-index-bar>
    </div>
  </div>
</template>
<script>
import Vue from 'vue';
import { IndexBar, IndexAnchor, Cell } from 'vant';
import axios from 'axios';
Vue.use(IndexBar).use(IndexAnchor).use(Cell);
export default {
  data() {
    return {
      cityGroupList: []
    }
  },
  async created() {
    const res = await axios.get('/static/city_data.json');
    this.cityGroupList = res.data;
  },
  methods: {
    selectCity(city) {
      localStorage.setItem('selected_city', JSON.stringify(city));
      this.$router.back();
    }
  }
}
</script>

Pull up to load more products

  1. Import and register List component:
import { List } from 'vant';
Vue.use(List);
  1. Add related data and methods in homepage:
data() {
  return {
    productList: [],
    isLoggedIn: false,
    isLoadingMore: false,
    isAllLoaded: false,
    currentPage: 1,
    pageSize: 10
  }
},
methods: {
  async handleLoadMore() {
    this.isLoadingMore = true;
    const token = localStorage.getItem('auth_token');
    const res = await axios.get(`/api/product?page=${this.currentPage}&size=${this.pageSize}&authToken=${token}`);
    this.isLoadingMore = false;
    if (res.data.code !== '20003') {
      this.isLoggedIn = true;
      const newProducts = res.data.data;
      if (newProducts.length === 0) {
        this.isAllLoaded = true;
      } else {
        this.productList = this.productList.concat(newProducts);
        this.currentPage += 1;
      }
    } else {
      this.isLoggedIn = false;
    }
  }
}
  1. Wrap product list with List component:
<van-list
  v-model="isLoadingMore"
  :finished="isAllLoaded"
  finished-text="No more products"
  @load="handleLoadMore"
>
  <ProductList v-if="isLoggedIn" :productList="productList" />
  <div v-else class="auth-tip">
    <p>Please login to view product content</p>
    <router-link to="/login" class="login-link">Go to login</router-link>
  </div>
</van-list>

Pull down to refresh

  1. Import and register PullRefresh component:
import { PullRefresh } from 'vant';
Vue.use(PullRefresh);
  1. Add related data and methods:
data() {
  return {
    // Other existing data
    isRefreshing: false
  }
},
methods: {
  async handleRefresh() {
    this.isRefreshing = true;
    const token = localStorage.getItem('auth_token');
    const res = await axios.get(`/api/product?page=1&size=${this.pageSize}&authToken=${token}`);
    this.isRefreshing = false;
    if (res.data.code !== '20003') {
      this.isLoggedIn = true;
      this.productList = res.data.data;
      this.currentPage = 2;
      this.isAllLoaded = false;
    } else {
      this.isLoggedIn = false;
    }
  }
}
  1. Wrap page content with PullRefresh component:
<van-pull-refresh v-model="isRefreshing" @refresh="handleRefresh">
  <van-swipe autoplay="3000" indicator-color="#fff">
    <van-swipe-item v-for="item in bannerList" :key="item.bid">
      <img :src="item.imgUrl" alt="banner" class="banner-img" />
    </van-swipe-item>
  </van-swipe>
  <van-list
    v-model="isLoadingMore"
    :finished="isAllLoaded"
    finished-text="No more products"
    @load="handleLoadMore"
  >
    <ProductList v-if="isLoggedIn" :productList="productList" />
    <div v-else class="auth-tip">
      <p>Please login to view product content</p>
      <router-link to="/login" class="login-link">Go to login</router-link>
    </div>
  </van-list>
</van-pull-refresh>

Back to top button

  1. Add iconfont import in public/index.html:
<link rel="stylesheet" href="//at.alicdn.com/t/font_1476238_uph8zgimp3.css" />
  1. Add back to top button in homepage template:
<span 
  class="back-top iconfont icon-fanhuidingbu" 
  v-show="showBackTop"
  @click="handleBackTop"
></span>
<style scoped>
.back-top {
  position: fixed;
  bottom: 60px;
  right: 15px;
  font-size: 32px;
  color: #666;
  z-index: 999;
}
</style>
  1. Add related logic:
data() {
  return {
    // Other existing data
    showBackTop: false
  }
},
watch: {
  currentPage(newVal) {
    this.showBackTop = newVal > 2;
  }
},
methods: {
  handleBackTop() {
    document.querySelector('.page-content').scrollTop = 0;
    this.showBackTop = false;
  }
}

Related Articles

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.