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:
- Register new users
- Authenticate users (login/logout)
- Manage user profiles and data
- Control access through permissions and roles
- 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:
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:
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:
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:
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:
import (
// other imports...
"github.com/dgrijalva/jwt-go"
"time"
)
Then create a login handler:
// 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:
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:
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:
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:
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:
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:
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:
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:
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:
{
"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:
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"johndoe", "password":"password123"}'
Expected Output:
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTA2MzYxMjIsInJvbGUiOiJ1c2VyIiwidXNlcl9pZCI6MiwidXNlcm5hbWUiOiJqb2huZG9lIn0.8vCTP-TH8hS_IntJvs4Kg27AH8I68qQ_zPgqY9lDsS8"
}
Get user profile:
curl -X GET http://localhost:8080/user/profile \
-H "Authorization: Bearer <token-from-login>"
Expected Output:
{
"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:
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:
{
"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:
- Email verification during registration
- Password reset via email
- OAuth integration for social logins
- Multi-factor authentication
- Session management
Additional Resources
Here are some resources to help you expand your knowledge of user management in Go:
- Gin Framework Documentation
- JWT Go Documentation
- Bcrypt Package Documentation
- OWASP Authentication Cheatsheet
- GORM Documentation
Exercises
- Implement a password reset functionality that sends a reset link via email
- Add account locking after multiple failed login attempts
- Create a user activity log to track important actions
- Implement email verification during registration
- Add the ability for users to delete their own accounts
- 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! :)