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.
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.
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 admin
import (
"github.com/jinzhu/gorm"
qadmin "github.com/qor/admin"
qauth "github.com/qor/auth"
"github.com/qor/redirect_back"
"github.com/qor/session/manager"
"gitlab.yalantis.com/gophers/qor-admin-panel-template/config"
"gitlab.yalantis.com/gophers/qor-admin-panel-template/models"
"gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/auth"
"gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/auth/password"
)
var (
defaultAdminConfig *qadmin.AdminConfig
defaultAuthConfig *qauth.Auth
)
// Load default admin and auth configurations on startup
func LoadDefaultConfigs(db *gorm.DB) {
// define custom auth strategy using password provider
defaultAuthConfig = password.New(qauth.Config{
DB: db,
UserModel: models.User{},
Redirector: &qauth.Redirector{redirect_back.New(&redirect_back.Config{ // nolint
SessionManager: manager.SessionManager,
FallbackPath: config.Config.AdminConfig.Auth.Redirector.FallbackPath,
// HACK: add "/auth" path to ignored paths to use the FallbackPath
// it allows you to redirect the user to the FallbackPath after successful login
IgnoredPrefixes: []string{"/auth"},
})},
// add view paths for auth to customize sign-in form
ViewPaths: []string{"gitlab.yalantis.com/gophers/qor-admin-panel-template/qor/auth/password/views"},
})
// Defile admin configuration with custom auth
defaultAdminConfig = &qadmin.AdminConfig{
DB: db,
Auth: auth.NewAdminAuth(defaultAuthConfig, config.Config.AdminConfig.Auth),
SiteName: config.Config.AdminConfig.SiteName,
}
}
func GetDefaultAdminConfig() *qadmin.AdminConfig {
return defaultAdminConfig
}
func GetDefaultAuthConfig() *qauth.Auth {
return defaultAuthConfig
}
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
}
Read also: How to Use Websockets in Golang: Best Tools and Step-by-Step Guide
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
},
})
}
Read also: Best Practices for Speeding Up JSON Encoding and Decoding in Go
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")
}})
}
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.
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.
Ten articles before and after
Best Tools and Main Reasons to Monitor Go Application Performance
Which Javascript Frameworks to Choose in 2021
Using RxSwift for Reactive Programming in Swift
How to Ensure Efficient Real-Time Big Data Analytics
Detailed Analysis of the Top Modern Database Solutions
How to Speed Up JSON Encoding and Decoding in Golang
How to Use GitLab Merge Requests for Code Review
How to Create a Restful API: Your Guide to Making a Developer-Friendly API
Practical Tips on Adding Push Notifications to iOS or Android Apps