Building a GVA-Based Admin System with Gin, Vue, and JWT

Login Handling with Sestion and JWT

Session-based authentication is required to restrict page access to logged-in users only. The Gin framework provides session middleware via github.com/gin-contrib/sessions.

import "github.com/gin-contrib/sessions"

rootRouter.GET("/login", func(c *gin.Context) {
    session := sessions.Default(c)
    session.Set("user", "feige")
    session.Save()
    c.JSON(http.StatusOK, gin.H{"message": "login success"})
})

Sessions store data server-side using a map structure. However, manually checking sessions on every route becomes verbose. A better approach is to use middleware, often implementing JWT for stateless authentication.

Structs and Methods

Struct methods defined in external packages require instantiation using new() or a composite literal.

type User struct {
    Name string
}

// Value receiver
func (u User) GetName() string {
    return u.Name
}

// Pointer receiver
func (u *User) SetName(name string) {
    u.Name = name
}

Use c.ShouldBindJSON for binding JSON request bodies to structs.

Configuration with Viper

Viper manages configuration files. Define a struct to map configuration values.

# config.yaml
app:
  host: "localhost"
  port: 8080
  debug: true
package config

type AppConfig struct {
    Host  string
    Port  int
    Debug bool
}
package main

import (
    "fmt"
    "github.com/spf13/viper"
)

func main() {
    v := viper.New()
    v.SetConfigType("yaml")
    v.AddConfigPath(".")
    v.SetConfigName("config")

    if err := v.ReadInConfig(); err != nil {
        panic(err)
    }

    var appCfg AppConfig
    if err := v.Unmarshal(&appCfg); err != nil {
        panic(err)
    }

    fmt.Printf("Config: %+v\n", appCfg)
}

Integrating GORM for Login

Define a user model and use GORM to interact with the database.

type User struct {
    gorm.Model
    Username string `gorm:"uniqueIndex;size:255"`
    Password string
}

HTTP status codes should be used carefully. For API responses, use http.StatusOK and handle bussiness errors in the response body.

CAPTCHA Verification

Use github.com/mojocn/base64Captcha to generate and verify image CAPTCHAs.

go get github.com/mojocn/base64Captcha

The server generates a base64-encoded image and a unique ID. The client sends the ID along with the user's input for verification.

JWT Authentication

JWT solves the stateless problem by encoding user data into a token. Install github.com/golang-jwt/jwt/v5.

type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    jwt.RegisteredClaims
}

func GenerateToken(userID uint, username string) (string, error) {
    claims := Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte("secret"))
}

Create a middleware to validate tokens.

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenString := c.GetHeader("Authorization")
        if tokenString == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
            return
        }
        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
            return []byte("secret"), nil
        })
        if err != nil || !token.Valid {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            return
        }
        c.Set("userID", claims.UserID)
        c.Set("username", claims.Username)
        c.Next()
    }
}

Token renewal can be implemented by checking remaining validity. If the token is about to expire, generate a new one and blacklist the old token using Redis or a database table.

State Management with Pinia (Vue 3)

Pinia manages shared state across Vue components.

Installation:

npm install pinia

Define a store:

// stores/user.js
import { defineStore } from 'pinia'
import axios from 'axios'

export const useUserStore = defineStore('user', {
    state: () => ({
        user: {},
        username: '',
        token: '',
        roles: []
    }),
    getters: {
        roleNames: (state) => state.roles.map(r => r.name).join(', ')
    },
    actions: {
        async login(credentials) {
            const resp = await axios.post('/api/login', credentials)
            if (resp.data.code === 20000) {
                this.user = resp.data.data.user
                this.username = resp.data.data.user.name
                this.token = resp.data.data.token
                this.roles = resp.data.data.roles
            } else {
                throw new Error(resp.data.msg)
            }
        }
    },
    persist: {
        key: 'user-store',
        storage: localStorage
    }
})

Use mapState and mapActions in options API:

<script>
import { mapState, mapActions } from 'pinia'
import { useUserStore } from '@/stores/user'

export default {
    computed: {
        ...mapState(useUserStore, ['username', 'token'])
    },
    methods: {
        ...mapActions(useUserStore, ['login'])
    }
}
</script>

For composition API, call the store directly:

<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>

Page Layout and Animations

Reset default styles and import Element Plus for UI components.

NProgress

Install and use for loading indicators:

npm install nprogress
import NProgress from 'nprogress'
NProgress.configure({ showSpinner: true })

router.beforeEach((to, from, next) => {
    NProgress.start()
    next()
})
router.afterEach(() => {
    NProgress.done()
})

Number Animation (Vue 3)

Install animated-number-vue3:

npm install animated-number-vue3
<template>
    <animated-number :from="0" :to="value" :duration="2000" />
</template>
<script setup>
import { ref } from 'vue'
const value = ref(4654)
</script>

Environment Configuration

Use .env files for different environments (development, production, testing). In Go, environment variables can be read with os.Getenv and cached; restart the application if variibles change.

Route Guards and Async Components

Use lazy loading for route components to improve performance:

const routes = [
    {
        path: '/dashboard',
        component: () => import('@/views/Dashboard.vue')
    }
]

Logging with Logrus and Lumberjack

Configure rolling logs:

func InitLogger() {
    log := logrus.New()
    log.SetFormatter(&logrus.TextFormatter{
        FullTimestamp:   true,
        TimestampFormat: "2006-01-02 15:04:05",
    })
    log.SetOutput(&lumberjack.Logger{
        Filename:   "./logs/app.log",
        MaxSize:    10,
        MaxBackups: 100,
        MaxAge:     30,
        Compress:   false,
    })
    log.SetLevel(logrus.DebugLevel)
    // Use log globally
}

Deployment Considerations

  • Build Go binaries for Linux/Windows.
  • Use Nginx as a reverse proxy with load balancing (upstream with weight for round-robin).
  • Prefer odd numbers of nodes in a cluster (e.g., 3 nodes) to avoid split-brain scenarios.
  • Dockerize applications and use container orchestration (Kubernetes or Swarm).
  • For microservices, consider gRPC for inter-service communication; avoid over-engineering small projects.

Tags: Gin vue JWT GORM Pinia

Posted on Tue, 02 Jun 2026 18:38:17 +0000 by jexx