Step-by-Step Guide to Deploying Admin Panel in Go using QOR

When building a platform-like application, you’ll need to create an admin panel to manage all app data. There’s a plethora of tools and approaches that allow developers to create admin panels without hassle. But such a variety of ready-made solutions creates difficulties with choosing the most effective one. 

At Yalantis, we’ve tried different tools for deploying admin panels using Go, and our choice is the QOR package. In this article, we provide you with a step-by-step guide to quickly build a configurable, easy-to-use admin panel for your application.

QOR overview

QOR is a set of libraries written in Go that abstract common features needed for ecommerce systems, content management systems, and business applications.

It contains several modules that will come in handy for working with content management systems and ecommerce apps:

  • Admin. QOR admin panels comply with Google Material Design principles. It allows developers to build responsive admin dashboard page for Golang and works well on both desktop and mobile devices. It is a great tool for ecommerce and CMS development.

  • Roles. Not all users are supposed to have rights for managing data. The roles package helps to define roles and permissions for controlling access to specific data fields. 

  • Inline Edit. This package allows developers to configure which content can be edited by which user role. It also provides convenient tools for defining and creating flexible, configurable widgets for frontend editing.

  • Worker. This package allows you to run batch processing and other time-consuming calculations in the background. 

  • Internationalization (i18n) and localization (i10n). When you’re going to expand your business abroad, you may need to translate or localize your application into new languages. These tools allow you to quickly provide multi-language support for your app.

QOR has well-written official documentation, so implementing this tool won’t take much time and effort. In the example below, we’ll tell you how to quickly create an admin panel in Golang for ecommerce website.

QOR admin panel

Step-by-step tutorial to deploying a QOR admin panel: our QOR example

To demonstrate the capabilities of QOR, let’s create an ecommerce store that has users, items, and orders. 

Step 1. Define the structure and architecture of the project.

Start with defining the project structure. Here’s how the structure of our project looks:

├── app
│   └── views
│       └── qor
│           └── dashboard.tmpl
├── config
│   ├── config.go
│   └── locales
│       └── en-US.yml
├── config.json
├── config_sample.json
├── docker-db
│   ├── docker-compose.yml
│   ├── init.sh
│   └── postgres-data
├── glide.lock
├── glide.yaml
├── handlers
│   └── router.go
├── logger
│   └── logger.go
├── main.go
├── Makefile
├── models
│   ├── address.go
│   ├── order.go
│   ├── order-item.go
│   ├── product.go
│   └── user.go
├── qor
│   ├── admin
│   │   ├── admin.go
│   │   ├── config.go
│   │   ├── handlers
│   │   │   └── admin.go
│   │   ├── permissions
│   │   │   ├── admin.go
│   │   │   ├── product.go
│   │   │   └── user.go
│   │   └── resources
│   │       ├── admin.go
│   │       ├── order.go
│   │       ├── order-item.go
│   │       ├── product.go
│   │       ├── resources.go
│   │       └── user.go
│   ├── auth
│   │   ├── admin-auth.go
│   │   └── password
│   │       ├── encryptor.go
│   │       ├── errors.go
│   │       ├── handlers.go
│   │       ├── password.go
│   │       └── views
│   └── roles
│       └── roles.go
├── README.md

Step 2. Integrate a database

We chose PostgreSQL 11 as the database management system for our app. To quickly integrate PostgreSQL into the project, we’ll use Docker.

Read also: How to Choose the Right Database for Your Website

Let’s start with defining the Docker configuration for the database. To do this, create two files in the Docker directory: init.sh and docker-compose.yml. These files must include the following lines of code:

docker-compose.yml

version: "3.3"
 
services:
 postgres:
   image: postgres
   container_name: postgres
   environment:
     - POSTGRES_USER=postgres
     - POSTGRES_PASSWORD=12345
   ports:
     - 5432:5432
   volumes:
     - ./init.sh:/docker-entrypoint-initdb.d/init.sh
     - ./postgres-data:/var/lib/postgresql/data

Init.sh

#!/bin/bash
set -e
for database in "qor-template";
   do
        psql -U postgres <<-EOSQL
        CREATE DATABASE "$database" WITH owner=postgres;
        EOSQL
    done

Step 3. Define models for the app

Next, let’s define the models for our application. For an ecommerce website, we’ll create models for User, Address, Product, Order, and OrderItem. Since qor/admin uses GORM for working with databases, we should embed gorm.Model in each of our models.

package models
 
import (
    "fmt"
 
    "github.com/jinzhu/gorm"
)
 
type User struct {
    gorm.Model
 
    Addresses []Address
 
    Email    string
    Password string
    Name     string
    Gender   string
    Role     string
}
 
// Implement qor/qor/context.go CurrentUser interface
func (u User) DisplayName() string {
    return fmt.Sprintf("%s(%s)", u.Name, u.Email)
}

package models
 
type Address struct {
    gorm.Model
 
    Street string
    Apt    string
}

package models
 
import "github.com/jinzhu/gorm"
 
type Product struct {
    gorm.Model
 
    Name        string
    Description string
    Code        string
    Active      bool
    Price       float64
}

package models
 
import (
    "time"
 
    "github.com/jinzhu/gorm"
)
 
type Order struct {
    gorm.Model
 
    OrderItems []OrderItem
    UserID     int
    User       User
 
    Amount          float64
    State           string
    ShippingAddress string
    ShippedAt       *time.Time
}

package models
 
import (
    "github.com/jinzhu/gorm"
)
 
type OrderItem struct {
    gorm.Model
 
    OrderID int
    Order   Order
 
    ProductID int
    Product   Product
 
    Price float64
}

Step 4. Admin and auth configuration

QOR admin provides a ready-made library for authentication. But this library has a small issue that allows every user to register as an administrator if you don’t process it separately. 

This approach doesn’t fit our needs, so we’ll use another approach to assigning admin roles. We’ll create the first admin profile manually, with a predefined login and password, using SQL queries, then give permission for this administrator to create other admin profiles. 

For this approach, we should first configure the authentication functionality, then configure the admin interface. We’ll take the qor/auth package as a base and improve it a bit. 

package models
 
import (
    "fmt"
 
    "github.com/jinzhu/gorm"
)
 
type User struct {
    gorm.Model
 
    Addresses []Address
 
    Email    string
    Password string
    Name     string
    Gender   string
    Role     string
}
 
// Implement qor/qor/context.go CurrentUser interface
func (u User) DisplayName() string {
    return fmt.Sprintf("%s(%s)", u.Name, u.Email)
}

package models
 
type Address struct {
    gorm.Model
 
    Street string
    Apt    string
}

package models
 
import "github.com/jinzhu/gorm"
 
type Product struct {
    gorm.Model
 
    Name        string
    Description string
    Code        string
    Active      bool
    Price       float64
}

package models
 
import (
    "time"
 
    "github.com/jinzhu/gorm"
)
 
type Order struct {
    gorm.Model
 
    OrderItems []OrderItem
    UserID     int
    User       User
 
    Amount          float64
    State           string
    ShippingAddress string
    ShippedAt       *time.Time
}

package models
 
import (
    "github.com/jinzhu/gorm"
)
 
type OrderItem struct {
    gorm.Model
 
    OrderID int
    Order   Order
 
    ProductID int
    Product   Product
 
    Price float64
}

Step 5. Implement qor/admin auth interface

When everything is configured, let’s implement the Auth interface from the qor/admin package. 

package auth
 
import (
    "github.com/qor/admin"
    "github.com/qor/auth"
    "github.com/qor/qor"
    "go.uber.org/zap"
 
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/config"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/logger"
)
 
// AdminAuth implements qor/admin auth interface
type AdminAuth struct {
    QorAuth    *auth.Auth
    LoginPath  string
    LogoutPath string
}
 
func NewAdminAuth(qorAuth *auth.Auth, c config.AdminAuthConfig) *AdminAuth {
    return &AdminAuth{
        QorAuth:    qorAuth,
        LoginPath:  c.LoginPath,
        LogoutPath: c.LogoutPath,
    }
}
 
func (a AdminAuth) LoginURL(c *admin.Context) string {
    return a.LoginPath
}
 
func (a AdminAuth) LogoutURL(c *admin.Context) string {
    return a.LogoutPath
}
 
func (a AdminAuth) GetCurrentUser(c *admin.Context) qor.CurrentUser {
    currentUser := a.QorAuth.GetCurrentUser(c.Request)
    if currentUser == nil {
        return nil
    }
 
    qorCurrentUser, ok := currentUser.(qor.CurrentUser)
    if !ok {
        logger.GetLog().Error("failed to cast to qor.CurrentUser", zap.Any("qorCurrentUser", qorCurrentUser))
    }
 
    return qorCurrentUser
}

Now let’s create our own encryptor. In the future, it will help us change the user password via the admin panel. In this example, we use the bcrypt algorithm; you can use any algorithm you want. 

Here’s the code for adding our custom encryptor:

package password
 
import "golang.org/x/crypto/bcrypt"
 
// Define custom encryptor to inject it to the auth config
type BcryptEncryptor struct{}
 
func NewBcryptEncryptor() BcryptEncryptor {
    return BcryptEncryptor{}
}
 
// Implement qor/auth interface for encryptor
func (be BcryptEncryptor) Digest(password string) (string, error) {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(hashedPassword), err
}
 
func (be BcryptEncryptor) Compare(hashedPassword, password string) error {
    return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

Next, we should add the custom authentication errors that signal users about failed authentication. Our custom errors look like this:

package password
 
import "errors"
 
// Declare custom auth errors
var (
    ErrPasswordConfirmationNotMatch = errors.New("password confirmation doesn't match password")
    ErrInvalidEmailOrPassword       = errors.New("invalid email or password")
)

Now we need to describe the login and registration handlers. If you have no need to implement registration, you can use a placeholder instead and delete routes for registration (as we did in the example below). We used the standard handlers and redefined them.

package password

import (
    "errors"
    "strings"
 
    "github.com/qor/auth"
    "github.com/qor/auth/auth_identity"
    "github.com/qor/auth/claims"
    "github.com/qor/auth/providers/password"
)
 
// Redefine authorization handler for auth
func AuthorizeHandler(context *auth.Context) (*claims.Claims, error) {
    var (
        authInfo auth_identity.Basic
        req      = context.Request
        tx       = context.Auth.GetDB(req)
    )
 
    provider, ok := context.Provider.(*password.Provider)
    if !ok {
        return nil, errors.New("failed to cast context.Provider to password.Provider")
    }
 
    err := req.ParseForm()
    if err != nil {
        return nil, err
    }
 
    authInfo.Provider = provider.GetName()
    authInfo.UID = strings.TrimSpace(req.Form.Get("login"))
 
    recordNotFound := tx.Model(context.Auth.AuthIdentityModel).
        Where("provider = ? ", authInfo.Provider).
        Where("uid = ?", authInfo.UID).
        Scan(&authInfo).
        RecordNotFound()
 
    if recordNotFound {
        return nil, ErrInvalidEmailOrPassword
    }
 
    if provider.Config.Confirmable && authInfo.ConfirmedAt == nil {
        currentUser, _ := context.Auth.UserStorer.Get(authInfo.ToClaims(), context)
        err := provider.Config.ConfirmMailer(authInfo.UID, context, authInfo.ToClaims(), currentUser)
        if err != nil {
            return nil, err
        }
 
        return nil, password.ErrUnconfirmed
    }
 
    if err := provider.Encryptor.Compare(authInfo.EncryptedPassword, strings.TrimSpace(req.Form.Get("password"))); err == nil {
        return authInfo.ToClaims(), err
    }
 
    return nil, ErrInvalidEmailOrPassword
}
 
// Redefine registration handler for auth
func RegisterHandler(context *auth.Context) (*claims.Claims, error) {
    err := context.Request.ParseForm()
    if err != nil {
        return nil, err
    }
 
    if context.Request.Form.Get("confirm_password") != context.Request.Form.Get("password") {
        return nil, ErrPasswordConfirmationNotMatch
    }
 
    return password.DefaultRegisterHandler(context)
}

Finally, we should declare the constructor describing the configuration for our password provider:

package password
 
import (
    "html/template"
    "net/http"
    "path/filepath"
 
    "github.com/qor/auth"
    "github.com/qor/auth/providers/password"
    "github.com/qor/i18n"
    "github.com/qor/i18n/backends/yaml"
    "github.com/qor/qor"
    "github.com/qor/qor/utils"
    "github.com/qor/render"
)
 
func New(config auth.Config) *auth.Auth {
    if config.Render == nil {
        // Initialize i18n with custom locales
        I18n := i18n.New(yaml.New(filepath.Join(utils.AppRoot, "config", "locales")))
 
        // Pass function "t" to views that allows you to use i18n translations
        config.Render = render.New(&render.Config{
            FuncMapMaker: func(render *render.Render, req *http.Request, w http.ResponseWriter) template.FuncMap {
                return template.FuncMap{
                    "t": func(key string, args ...interface{}) template.HTML {
                        return I18n.T(utils.GetLocale(&qor.Context{Request: req}), key, args...)
                    },
                }
            },
        })
    }
 
    // Define new custom auth with password provider
    pwdAuth := auth.New(&config)
    pwdAuth.RegisterProvider(password.New(&password.Config{
        Confirmable:      false,
        RegisterHandler:  RegisterHandler,
        AuthorizeHandler: AuthorizeHandler,
        Encryptor:        BcryptEncryptor{},
    }))
 
    if pwdAuth.Config.DB != nil {
        // Automigrate AuthIdentity model
        pwdAuth.Config.DB.AutoMigrate(pwdAuth.Config.AuthIdentityModel)
    }
 
    return pwdAuth
}

Step 6. Implement admin startup loader

QOR Golang admin framework has resources (i.e. user resource, item resource) and operations (i.e. edit, add, delete). 

During this step, we connect resources (that we’ll define later) to our admin panel. We’ll do this with the help of the Load function.

package admin
import (
    "sync"
 
    qadmin "github.com/qor/admin"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin/resources"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/roles"
)
 
type admin struct {
    Once  sync.Once
    Admin *qadmin.Admin
}
 
var qorAdmin admin
 
func GetAdmin() *qadmin.Admin {
    return qorAdmin.Admin
}
 
// Load initializes admin panel with resources on startup
func Load(adminCfg *qadmin.AdminConfig) error {
    var err error
 
    qorAdmin.Once.Do(func() {
        if e := roles.Load(); e != nil {
            err = e
            return
        }
 
        qorAdmin.Admin = qadmin.New(adminCfg)
        resources.AddResources(qorAdmin.Admin)
    })
 
    return err
}

Step 7. Register roles

Now let’s declare roles for the admin panel. Thanks to roles, you can restrict access to data and operations for some users. In the example below, we created admin and manager roles. If you want users to somehow collaborate with the admin panel (for instance, to edit or add items), you should also create the corresponding role and configure access rights to the resources. 

package roles
 
import (
    "net/http"
 
    "github.com/qor/roles"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
)
 
// Role names
const (
    Admin   = "admin"
    Manager = "manager"
)
 
// Definition of "admin" and "not admin" roles
var (
    RolesList     = []string{Admin, Manager}
    NotAdminRoles = []string{Manager}
)
 
// Register roles on startup
func Load() error {
    roles.Register(Admin, func(req *http.Request, currentUser interface{}) bool {
        usr, ok := currentUser.(*models.User)
        if !ok {
            return false
        }
        return usr.Role == Admin
    })
 
    roles.Register(Manager, func(req *http.Request, currentUser interface{}) bool {
        usr, ok := currentUser.(*models.User)
        if !ok {
            return false
        }
        return usr.Role == Manager
    })
 
    return nil
}

Step 8. Working with resources

To work with data stored in our database, we need to add resources connected with the models declared above. Resources allow us to flexibly configure the connections between models, as well as to configure access rights for certain operations.

First, let’s add resources to the admin panel. 

Product resource

package resources
 
import (
    "github.com/jinzhu/gorm"
    qadmin "github.com/qor/admin"
    "github.com/qor/qor"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin/permissions"
)
 
func (r resources) AddProducts() {
    // Register product resource with custom permissions
    product := r.Admin.AddResource(&models.Product{}, &qadmin.Config{
        Permission: permissions.Product,
    })
 
    // Customize form view
    product.NewAttrs(
        &qadmin.Section{
            Title: "Basic Information",
            Rows: [][]string{
                {"Name"},
                {"Code", "Price"},
            },
        },
        &qadmin.Section{
            Title: "Advanced Information",
            Rows: [][]string{
                {"Description"},
                {"Active"},
            },
        },
    )
 
    // Display the "Description" field as rich editor
    product.Meta(&qadmin.Meta{Name: "Description", Type: "rich_editor"})
 
    // Define custom scope(filter) to display only active products
    product.Scope(&qadmin.Scope{Name: "Active", Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
        return db.Where("active = ?", true)
    }})
 
    // Add custom action for resource
    product.Action(&qadmin.Action{
        Name:  "Enable",
        Modes: []string{"batch", "edit", "show", "menu_item", "collection"}, // Specify modes the button will be displayed in
        Handler: func(actionArgument *qadmin.ActionArgument) error {
            for _, record := range actionArgument.FindSelectedRecords() { // Loop through selected records in bulk edit mode
                actionArgument.Context.GetDB().Model(record.(*models.Product)).Update("Active", true)
            }
            return nil
        },
    })
}

Product resource QOR

Order item resource

package resources
 
import (
    qadmin "github.com/qor/admin"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
)
 
func (r resources) AddOrderItems() {
    // Define hidden order items resource
    orderItem := r.Admin.AddResource(&models.OrderItem{}, &qadmin.Config{Invisible: true})
 
    // Allow user to only select one product per order item when creating an order
    orderItem.Meta(&qadmin.Meta{Name: "Product", Resource: r.Admin.GetResource("Product"), Type: "select_one"})
}

Order resource

package resources
 
import (
    "github.com/jinzhu/gorm"
    qadmin "github.com/qor/admin"
    "github.com/qor/qor"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
)
 
func (r resources) AddOrders() {
    // Register order resource
    order := r.Admin.AddResource(&models.Order{})
 
    // Allow you to select only one user the order will be connected to
    order.Meta(&qadmin.Meta{Name: "User",
        Resource: r.Admin.GetResource("User"),
        Type:     "select_one",
    })
 
    order.Meta(&qadmin.Meta{Name: "OrderItems"})
 
    // Define the attributes that should be shown on specific pages/forms
    order.IndexAttrs("User", "OrderItems", "Amount", "State", "ShippingAddress")
    order.NewAttrs("User", "OrderItems", "Amount", "State", "ShippingAddress")
    order.EditAttrs("User", "OrderItems", "Amount", "State", "ShippingAddress", "ShippedAt")
 
    // Define custom scopes/filters
    order.Scope(&qadmin.Scope{Name: "Paid", Group: "State", Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
        return db.Where("state = ?", "paid")
    }})
    order.Scope(&qadmin.Scope{Name: "Shipped", Group: "State", Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
        return db.Where("state = ?", "shipped")
    }})
}

Order resource

Next, we should create user resources. As far as our application has managers and admins, we should add and configure resources for each role. Both admins and users will be stored in one table; their roles will be defined in the role column. 

Regular user resource 

package resources
 
import (
    "errors"
 
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/auth/password"
 
    "github.com/jinzhu/gorm"
    qadmin "github.com/qor/admin"
    "github.com/qor/qor"
    "github.com/qor/qor/resource"
    qroles "github.com/qor/roles"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin/handlers"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin/permissions"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/roles"
)
 
func (r resources) AddUsers() {
 
    // Register users resource
    user := r.Admin.AddResource(
        &models.User{},
        &qadmin.Config{
            Menu:       []string{"User Management"}, // Define menu group for this resource
            Permission: permissions.User,            // Define custom permissions
        },
    )
    user.IndexAttrs("-Password") // Exclude password attribute from the index page
 
    // Select only one value from the predefined list
    user.Meta(&qadmin.Meta{Name: "Gender", Config: &qadmin.SelectOneConfig{Collection: []string{"Male", "Female", "Unknown"}}})
 
    // User's role can be set only to "not admin" role
    user.Meta(&qadmin.Meta{Name: "Role", Config: &qadmin.SelectOneConfig{Collection: roles.NotAdminRoles}})
 
    // Define field type as "password"
    // and add custom permissions to the field
    user.Meta(&qadmin.Meta{Name: "Password", Type: "password", Permission: qroles.Allow(qroles.Read, roles.Admin, roles.Manager)})
 
    // Add default scope/filter to show only users that are not admins
    user.Scope(&qadmin.Scope{
        Name:    "Not Admin",
        Default: true,
        Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
            return db.Where("role <> ?", roles.Admin)
        },
        Visible: func(context *qadmin.Context) bool { // Make the scope/filter invisible
            return false
        },
    })
 
    // Redefine SaveHandler to synchronize password and other auth information across User and AuthIdentity models
    user.SaveHandler = handlers.UserSaveHandler(user)
 
    // Encrypt the password after form submission but before running gorm callbacks and saving to the database
    user.AddProcessor(&resource.Processor{
        Name: "encode_user_password",
        Handler: func(value interface{}, metaValues *resource.MetaValues, context *qor.Context) error {
            usr, ok := value.(*models.User)
            if !ok {
                return errors.New("invalid model passed")
            }
 
            pwd, err := password.NewBcryptEncryptor().Digest(usr.Password)
            if err != nil {
                return err
            }
 
            usr.Password = pwd
 
            return nil
        },
    })
}

Admin user resource

package resources
 
import (
    "errors"
 
    "github.com/jinzhu/gorm"
    qadmin "github.com/qor/admin"
    "github.com/qor/qor"
    "github.com/qor/qor/resource"
    qroles "github.com/qor/roles"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin/handlers"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin/permissions"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/auth/password"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/roles"
)
 
func (r resources) AddAdmins() {
    // Register admin resource
    adm := r.Admin.AddResource(
        &models.User{},
        &qadmin.Config{
            Name:       "Admin",
            Menu:       []string{"User Management"}, // Define menu group for this resource
            Permission: permissions.Admin,           // Define custom permissions
        },
    )
    adm.IndexAttrs("-Password") // Exclude password attribute from the index page
 
    // Select only one value from the predefined list
    adm.Meta(&qadmin.Meta{Name: "Gender", Config: &qadmin.SelectOneConfig{Collection: []string{"Male", "Female", "Unknown"}}})
 
    // User's role can be set only to admin roles
    adm.Meta(&qadmin.Meta{Name: "Role", Config: &qadmin.SelectOneConfig{Collection: []string{roles.Admin}}})
 
    // Define field type as "password"
    // and add custom permissions to the field
    adm.Meta(&qadmin.Meta{
        Name:       "Password",
        Type:       "password",
        Permission: qroles.Allow(qroles.CRUD, roles.Admin),
    })
 
    // Add default scope/filter to show only admin users
    adm.Scope(&qadmin.Scope{
        Name:    "Admin",
        Default: true,
        Handler: func(db *gorm.DB, context *qor.Context) *gorm.DB {
            return db.Where("role = ?", roles.Admin)
        },
        Visible: func(context *qadmin.Context) bool {
            return false
        },
    })
 
    // Redefine SaveHandler to synchronize password and other auth information across User and AuthIdentity models
    adm.SaveHandler = handlers.UserSaveHandler(adm)
 
    // Encrypt the password after form submission but before running gorm callbacks and saving to the database
    adm.AddProcessor(&resource.Processor{
        Name: "encode_user_password",
        Handler: func(value interface{}, metaValues *resource.MetaValues, context *qor.Context) error {
            adminUser, ok := value.(*models.User)
            if !ok {
                return errors.New("invalid model passed")
            }
 
            pwd, err := password.NewBcryptEncryptor().Digest(adminUser.Password)
            if err != nil {
                return err
            }
 
            adminUser.Password = pwd
 
            return nil
        },
    })
}

After we’ve created all resources, our task is to enable admins to easily manage these resources in the admin panel. For this, we should implement a helper for adding resources. 

package resources

import (
    qadmin "github.com/qor/admin"
)
 
type resources struct {
    Admin *qadmin.Admin
}
 
// AddResources registers resources to the admin
func AddResources(qorAdmin *qadmin.Admin) {
    r := resources{Admin: qorAdmin}
 
    r.AddAdmins()
    r.AddUsers()
    r.AddProducts()
    r.AddOrderItems()
    r.AddOrders()
}

Step 9. Customize permissions

To configure access permissions, we’ll use the qor/roles library. But before using this tool, we strongly recommend you read its official documentation, since qor/roles has a couple of pitfalls you should know about. 

  • Admin permissions

package permissions
 
import (
    qroles "github.com/qor/roles"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/roles"
)
 
// Admin describes the permissions for "admin" user
var Admin = qroles.Allow(qroles.CRUD, roles.Admin)

Not admin permissions
package permissions
 
import (
    qroles "github.com/qor/roles"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/roles"
)
 
// User describes the permissions for "not admin" user
var User = qroles.
    Allow(qroles.CRUD, roles.Admin).
    Allow(qroles.Read, roles.Manager)
  • Product permissions

package permissions
 
import (
    qroles "github.com/qor/roles"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/roles"
)
 
// Product describes the permissions for product resource
var Product = qroles.
    Allow(qroles.CRUD, roles.Admin).
    Deny(qroles.Delete, roles.Manager)

Step 10. Rewrite the save handler for users to keep passwords synced

The qor/admin package uses two models: one for working with users (the UserModel) and one for authenticating them (the AuthIdentityModel). We recommend you synchronize these models rather than allow admins to edit  the AuthIdentityModel while editing the UserModel. 

When describing admin and user resources, we’ve added processes that encrypt passwords after submitting the form but before passing callbacks and saving data in the database. 

Now let’s configure the synchronization of passwords and other fields between these two models. For this, we define our own handler.  

package handlers

import (
    "errors"
    "fmt"
    "time"
 
    qadmin "github.com/qor/admin"
    "github.com/qor/auth/auth_identity"
    "github.com/qor/qor"
    "github.com/qor/roles"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
)
 
// UserSaveHandler provides synchronization between UserModel and AuthIdentityModel
// and keeps emails and passwords synced
func UserSaveHandler(usr *qadmin.Resource) func(rec interface{}, ctx *qor.Context) error {
    return func(rec interface{}, ctx *qor.Context) error {
        tx := usr.GetAdmin().AdminConfig.DB.Begin()
// Start database transaction
        if err := tx.Error; err != nil {
            return err
        }
 
        ctx.SetDB(tx) 
// Use recently opened transaction for default save handler
 
        saveHandler := defaultSaveHandler(usr)
        if err := saveHandler(rec, ctx); err != nil {
            tx.Rollback() // Roll back the transaction if an error occurs
            return err
        }
 
        usrRec, ok := rec.(*models.User)
        if !ok {
            tx.Rollback()
            return errors.New("failed to cast record to User model")
        }
 
        // Find AuthIdentity record if it exists
        var authRec auth_identity.AuthIdentity
        authRecNotFound := tx.Where("provider = ?", "password").
            Where("uid = ?", usrRec.Email).
            Where("user_id = ?", fmt.Sprint(usrRec.ID)).
            First(&authRec).RecordNotFound()
 
        // Add confirmation time if auth is not confirmable
        if authRecNotFound {
            now := time.Now()
            authRec.ConfirmedAt = &now
        }
 
        // Sync provider, password, and uid fields between the User and AuthIdentity models
        authRec.Provider = "password"
        authRec.UID = usrRec.Email
        authRec.UserID = fmt.Sprint(usrRec.ID)
        authRec.EncryptedPassword = usrRec.Password
 
        if err := tx.Save(&authRec).Error; err != nil {
            tx.Rollback()
            return err
        }
 
        return tx.Commit().Error
    }
}
 
// Default qor/admin save handler
func defaultSaveHandler(res *qadmin.Resource) func(result interface{}, context *qor.Context) error {
    return func(result interface{}, context *qor.Context) error {
        if (context.GetDB().NewScope(result).PrimaryKeyZero() &&
            res.HasPermission(roles.Create, context)) || // Has create permission
            res.HasPermission(roles.Update, context) { // Has update permission
            return context.GetDB().Save(result).Error
        }
        return roles.ErrPermissionDenied
    }
}

Step 11. Add HTTP router 

Next, we need to add HTTP routers. For our project, we use the gorilla/mux package that implements a request router and dispatcher. The QOR documentation describes how to integrate QOR with popular Go frameworks and routers. We’ll use code from it to add routers. 

We need only a password provider, so we’ll register all routers for it (bear in mind that we don’t need a registration router for our project). 

We can use regular expressions to match routers with handlers, but this approach can cause performance issues, especially when you need all routers for only one provider. So we won’t use this approach. Instead, we’ll register all routers manually. Thanks to this, we’ll be sure that they’re all right.

package handlers
 
import (
    "net/http"
 
    "github.com/gorilla/mux"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/config"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin"
)
 
// NewRouter creates a router for URL-to-service mapping
func NewRouter() *mux.Router {
    var (
        r = mux.NewRouter()
 
        adminMux     = http.NewServeMux()
        authServeMux = admin.GetDefaultAuthConfig().NewServeMux()
        authPref     = config.Config.AdminConfig.Auth.PathPrefix
    )
 
    // Integrate admin with gorilla/mux router
    admin.GetAdmin().MountTo(config.Config.AdminConfig.MountRoute, adminMux)
    r.PathPrefix(config.Config.AdminConfig.MountRoute).Handler(adminMux)
 
    r.PathPrefix(authPref + "assets/").Handler(authServeMux) // Handle assets
 
    // Admin auth routes without registration routes for password provider
    // Use `r.PathPrefix(authPref).Handler(authServeMux)` if registration is required
    r.Handle(authPref+"login", authServeMux)
    r.Handle(authPref+"logout", authServeMux)
    r.Handle(authPref+"password/login", authServeMux)
    r.Handle(authPref+"password/new", authServeMux)
    r.Handle(authPref+"password/recover", authServeMux)
 
    return r
}

Final steps

We’re almost done with deploying the admin panel. It’s time to add some flourishes. Before deploying the admin panel, you should embed assets (HTML, CSS, JavaScript) to the Go binary file. Or you can put them in the same directory with the binary file on the server. Also, we highly recommend you read the deployment instructions in the QOR documentation.  QOR has many Golang dashboard templates, you can use them or customize templates.  

Adding order

Customize your frontend admin dashboard and use the following code to start the application:

package main
 
import (
    "fmt"
    "log"
    "net/http"
    "os"
 
    "go.uber.org/zap"
 
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/postgres"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/config"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/handlers"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/logger"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
    "gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/admin"
)
 
// defaultConfigPath defines a path to the JSON config file
const defaultConfigPath = "config.json"
 
func main() {
    err := config.Load(defaultConfigPath)
    if err != nil {
        log.Fatalf("Failed to initialize Config: %v", err)
    }
 
    err = logger.Load()
    if err != nil {
        log.Fatalf("Failed to initialize logger: %v", err)
    }
 
    postgresConnStr := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s",
        config.Config.Postgres.Host,
        config.Config.Postgres.Port,
        config.Config.Postgres.User,
        config.Config.Postgres.Database,
        config.Config.Postgres.Password,
        config.Config.Postgres.SSLMode)
    db, err := gorm.Open("postgres", postgresConnStr)
    if err != nil {
        logger.GetLog().Fatal("Failed to connect to the database", zap.Error(err))
    }
 
    db.LogMode(true)
    db.AutoMigrate(
        &models.User{},
        &models.Product{},
        &models.Address{},
        &models.User{},
        &models.Order{},
        &models.OrderItem{},
    )
 
    admin.LoadDefaultConfigs(db)
    err = admin.Load(admin.GetDefaultAdminConfig())
    if err != nil {
        logger.GetLog().Fatal("Failed to load admin", zap.Error(err))
    }
 
    server := &http.Server{
        Addr:    config.Config.ListenURL,
        Handler: handlers.NewRouter(),
    }
 
    fmt.Printf("Listening on %s\n", config.Config.ListenURL)
    err = server.ListenAndServe()
    if err != nil {
        logger.GetLog().Fatal("Failed to initialize HTTP server", zap.Error(err))
        os.Exit(1)
    }
}

Now your admin panel is ready for users. As you can see, -QOR is a great open-source SDK for e-commerce app development. 

We hope this article will help you create convenient admin panels for your web or mobile applications. By the way, if you would like us to help you create your app, you can always write us

4.8/ 5.0
Article rating
26
Reviews
Remember those Facebook reactions? Well, we aren't Facebook but we love reactions too. They can give us valuable insights on how to improve what we're doing. Would you tell us how you feel about this article?
Learn more about how we create web apps

We provide custom web app development services

Discover our web development services

We use cookies to personalize our service and to improve your experience on the website and its subdomains. We also use this information for analytics.

More info