Skip to main content

Gin User Management

User management is a fundamental aspect of modern web applications. In this tutorial, we'll explore how to implement comprehensive user management features in a Gin web application, building on authentication concepts to create a complete system for handling users.

What Is User Management?

User management refers to the systems and processes that allow applications to:

  1. Register new users
  2. Authenticate users (login/logout)
  3. Manage user profiles and data
  4. Control access through permissions and roles
  5. Handle account operations like password resets

Let's build a complete user management system in Gin step by step.

Setting Up Our Project

First, we need to set up our Gin project with the necessary dependencies:

go
package main

import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"golang.org/x/crypto/bcrypt"
)

var db *gorm.DB
var err error

func main() {
// Initialize database
db, err = gorm.Open("sqlite3", "users.db")
if err != nil {
panic("Failed to connect to database")
}
defer db.Close()

// Set up routes
r := gin.Default()
setupRoutes(r)

r.Run(":8080")
}

func setupRoutes(r *gin.Engine) {
// We'll add our routes here
}

User Model

Let's define a User model with essential fields:

go
type User struct {
ID uint `json:"id" gorm:"primary_key"`
Username string `json:"username" gorm:"unique"`
Email string `json:"email" gorm:"unique"`
Password string `json:"-" gorm:"not null"` // "-" means don't show in JSON responses
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Role string `json:"role" gorm:"default:'user'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func init() {
// Auto-migrate creates the table if it doesn't exist
db.AutoMigrate(&User{})
}

User Registration

Let's implement user registration functionality:

go
func registerUser(c *gin.Context) {
var user User
var existingUser User

if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

// Check if username already exists
if db.Where("username = ?", user.Username).First(&existingUser).RowsAffected > 0 {
c.JSON(409, gin.H{"error": "Username already exists"})
return
}

// Check if email already exists
if db.Where("email = ?", user.Email).First(&existingUser).RowsAffected > 0 {
c.JSON(409, gin.H{"error": "Email already exists"})
return
}

// Hash password before storing
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(500, gin.H{"error": "Could not hash password"})
return
}

user.Password = string(hashedPassword)
user.Role = "user" // Default role

// Save user to database
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "Could not register user"})
return
}

// Don't return the password
user.Password = ""

c.JSON(201, gin.H{
"message": "User registered successfully",
"user": user,
})
}

Now, add this handler to our routes:

go
func setupRoutes(r *gin.Engine) {
auth := r.Group("/auth")
{
auth.POST("/register", registerUser)
}
}

User Login and JWT Authentication

For authentication, we'll use JWT (JSON Web Tokens). First, let's add the JWT package to our imports:

go
import (
// other imports...
"github.com/dgrijalva/jwt-go"
"time"
)

Then create a login handler:

go
// Define a secret key for JWT signing
var jwtSecret = []byte("your_secret_key") // In production, use environment variables

func loginUser(c *gin.Context) {
var loginData struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}

if err := c.ShouldBindJSON(&loginData); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

var user User
if db.Where("username = ?", loginData.Username).First(&user).RecordNotFound() {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}

// Compare hashed password with provided password
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginData.Password))
if err != nil {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}

// Generate JWT token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
"username": user.Username,
"role": user.Role,
"exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours
})

tokenString, err := token.SignedString(jwtSecret)
if err != nil {
c.JSON(500, gin.H{"error": "Could not generate token"})
return
}

c.JSON(200, gin.H{
"message": "Login successful",
"token": tokenString,
})
}

Add the login route:

go
func setupRoutes(r *gin.Engine) {
auth := r.Group("/auth")
{
auth.POST("/register", registerUser)
auth.POST("/login", loginUser)
}
}

JWT Authentication Middleware

Now, let's create a middleware to protect routes that require authentication:

go
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}

// The header should be in the format: "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(401, gin.H{"error": "Authorization header format must be Bearer <token>"})
c.Abort()
return
}

tokenString := parts[1]

// Parse the JWT token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Check the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})

if err != nil {
c.JSON(401, gin.H{"error": err.Error()})
c.Abort()
return
}

// Check if the token is valid
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Store user info in the context for later use
c.Set("user_id", claims["user_id"])
c.Set("username", claims["username"])
c.Set("role", claims["role"])
c.Next()
} else {
c.JSON(401, gin.H{"error": "Invalid token"})
c.Abort()
return
}
}
}

User Profile Management

Now that we have authentication set up, let's implement user profile management:

go
func getUserProfile(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(500, gin.H{"error": "User ID not found in context"})
return
}

var user User
if db.First(&user, userID).RecordNotFound() {
c.JSON(404, gin.H{"error": "User not found"})
return
}

// Don't expose the password
user.Password = ""

c.JSON(200, gin.H{
"user": user,
})
}

func updateUserProfile(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(500, gin.H{"error": "User ID not found in context"})
return
}

var user User
if db.First(&user, userID).RecordNotFound() {
c.JSON(404, gin.H{"error": "User not found"})
return
}

var updateData struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}

if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

// Update fields
if updateData.FirstName != "" {
user.FirstName = updateData.FirstName
}
if updateData.LastName != "" {
user.LastName = updateData.LastName
}
if updateData.Email != "" {
// Check if email is already in use by another user
var existingUser User
if db.Where("email = ? AND id != ?", updateData.Email, userID).First(&existingUser).RowsAffected > 0 {
c.JSON(409, gin.H{"error": "Email already in use by another user"})
return
}
user.Email = updateData.Email
}

// Save updates
db.Save(&user)

// Don't expose the password
user.Password = ""

c.JSON(200, gin.H{
"message": "Profile updated successfully",
"user": user,
})
}

Password Change Functionality

Users should be able to change their passwords securely:

go
func changePassword(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(500, gin.H{"error": "User ID not found in context"})
return
}

var user User
if db.First(&user, userID).RecordNotFound() {
c.JSON(404, gin.H{"error": "User not found"})
return
}

var passwordData struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}

if err := c.ShouldBindJSON(&passwordData); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

// Verify current password
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(passwordData.CurrentPassword))
if err != nil {
c.JSON(401, gin.H{"error": "Current password is incorrect"})
return
}

// Hash and save new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(500, gin.H{"error": "Could not hash new password"})
return
}

user.Password = string(hashedPassword)
db.Save(&user)

c.JSON(200, gin.H{
"message": "Password changed successfully",
})
}

Role-Based Access Control

Let's implement role-based access control with an admin middleware:

go
func adminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
role, exists := c.Get("role")
if !exists {
c.JSON(500, gin.H{"error": "Role not found in context"})
c.Abort()
return
}

if role != "admin" {
c.JSON(403, gin.H{"error": "Admin access required"})
c.Abort()
return
}

c.Next()
}
}

func getAllUsers(c *gin.Context) {
var users []User
db.Find(&users)

// Remove passwords from response
for i := range users {
users[i].Password = ""
}

c.JSON(200, gin.H{
"users": users,
})
}

func deleteUser(c *gin.Context) {
userID := c.Param("id")

var user User
if db.First(&user, userID).RecordNotFound() {
c.JSON(404, gin.H{"error": "User not found"})
return
}

db.Delete(&user)

c.JSON(200, gin.H{
"message": "User deleted successfully",
})
}

Setting Up All Routes

Now, let's organize all our routes with appropriate middleware:

go
func setupRoutes(r *gin.Engine) {
// Public routes
auth := r.Group("/auth")
{
auth.POST("/register", registerUser)
auth.POST("/login", loginUser)
}

// Protected user routes
user := r.Group("/user")
user.Use(authMiddleware())
{
user.GET("/profile", getUserProfile)
user.PUT("/profile", updateUserProfile)
user.POST("/change-password", changePassword)
}

// Admin routes
admin := r.Group("/admin")
admin.Use(authMiddleware())
admin.Use(adminMiddleware())
{
admin.GET("/users", getAllUsers)
admin.DELETE("/users/:id", deleteUser)
}
}

Complete Example Application

Here's our complete user management system for a Gin application:

go
package main

import (
"fmt"
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/dgrijalva/jwt-go"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"golang.org/x/crypto/bcrypt"
)

var db *gorm.DB
var jwtSecret = []byte("your_secret_key")

type User struct {
ID uint `json:"id" gorm:"primary_key"`
Username string `json:"username" gorm:"unique"`
Email string `json:"email" gorm:"unique"`
Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Role string `json:"role" gorm:"default:'user'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func main() {
// Initialize database
var err error
db, err = gorm.Open("sqlite3", "users.db")
if err != nil {
panic("Failed to connect to database")
}
defer db.Close()

// Auto migrate the schema
db.AutoMigrate(&User{})

// Create admin user if not exists
createDefaultAdmin()

// Initialize Gin router
r := gin.Default()
setupRoutes(r)

r.Run(":8080")
}

func createDefaultAdmin() {
var adminUser User
if db.Where("username = ?", "admin").First(&adminUser).RecordNotFound() {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
adminUser = User{
Username: "admin",
Email: "[email protected]",
Password: string(hashedPassword),
FirstName: "Admin",
LastName: "User",
Role: "admin",
}
db.Create(&adminUser)
fmt.Println("Default admin user created")
}
}

func setupRoutes(r *gin.Engine) {
// Routes defined as above
}

// Handlers and middleware defined as above

Testing Our User Management System

Let's test our API with curl commands (you can use Postman or other tools as well):

Register a new user:

bash
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"johndoe", "email":"[email protected]", "password":"password123", "first_name":"John", "last_name":"Doe"}'

Expected Output:

json
{
"message": "User registered successfully",
"user": {
"id": 2,
"username": "johndoe",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"role": "user",
"created_at": "2023-07-28T14:15:22Z",
"updated_at": "2023-07-28T14:15:22Z"
}
}

Login with the new user:

bash
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"johndoe", "password":"password123"}'

Expected Output:

json
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTA2MzYxMjIsInJvbGUiOiJ1c2VyIiwidXNlcl9pZCI6MiwidXNlcm5hbWUiOiJqb2huZG9lIn0.8vCTP-TH8hS_IntJvs4Kg27AH8I68qQ_zPgqY9lDsS8"
}

Get user profile:

bash
curl -X GET http://localhost:8080/user/profile \
-H "Authorization: Bearer <token-from-login>"

Expected Output:

json
{
"user": {
"id": 2,
"username": "johndoe",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"role": "user",
"created_at": "2023-07-28T14:15:22Z",
"updated_at": "2023-07-28T14:15:22Z"
}
}

Update profile:

bash
curl -X PUT http://localhost:8080/user/profile \
-H "Authorization: Bearer <token-from-login>" \
-H "Content-Type: application/json" \
-d '{"first_name":"Jonathan", "last_name":"Doe Jr."}'

Expected Output:

json
{
"message": "Profile updated successfully",
"user": {
"id": 2,
"username": "johndoe",
"email": "[email protected]",
"first_name": "Jonathan",
"last_name": "Doe Jr.",
"role": "user",
"created_at": "2023-07-28T14:15:22Z",
"updated_at": "2023-07-28T14:18:45Z"
}
}

Summary

In this tutorial, we've built a comprehensive user management system for a Gin application, including:

  • User registration and authentication with JWT
  • Secure password storage with bcrypt
  • User profile management
  • Password changing functionality
  • Role-based access control
  • Admin functionality for user management

This system provides a solid foundation for building secure web applications with Gin. You can extend it with features like:

  1. Email verification during registration
  2. Password reset via email
  3. OAuth integration for social logins
  4. Multi-factor authentication
  5. Session management

Additional Resources

Here are some resources to help you expand your knowledge of user management in Go:

  1. Gin Framework Documentation
  2. JWT Go Documentation
  3. Bcrypt Package Documentation
  4. OWASP Authentication Cheatsheet
  5. GORM Documentation

Exercises

  1. Implement a password reset functionality that sends a reset link via email
  2. Add account locking after multiple failed login attempts
  3. Create a user activity log to track important actions
  4. Implement email verification during registration
  5. Add the ability for users to delete their own accounts
  6. Implement multi-factor authentication using SMS or authenticator apps

By mastering user management in Gin, you'll be well-equipped to build secure and user-friendly web applications in Go.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)