Skip to content

Commit

Permalink
[WIP] QOR Article
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Lhussiez committed Feb 12, 2019
1 parent 538e026 commit 1c2f348
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 15 deletions.
27 changes: 27 additions & 0 deletions code/qor/main.go
@@ -0,0 +1,27 @@
package main

import (
"time"

"github.com/jinzhu/gorm"
uuid "github.com/satori/go.uuid"
"github.com/sirupsen/logrus"
)

type product struct {
ID uuid.UUID `gorm:"primary_key;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time

Name string
Price int
}

func main() {
db, err := gorm.Open("postgres", "host=myhost port=myport user=gorm dbname=gorm password=mypassword")
if err != nil {
logrus.WithError(err).Fatal("Couldn't initialize database connection")
}
defer db.Close()
}
Binary file added pages/assets/qor-admin/banner.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions pages/gorm-gotchas.md
Expand Up @@ -368,6 +368,72 @@ func (a *Article) UnmarshalMetaData() error {
Now you can marshal before saving your article, and unmarshal once you retrieve
it from the database !

## Using UUID as Primary Key

If you wish to use the UUID extension for postgres, this section is for you.
First we'll declare our model with a slight change, the ID won't be an `uint`
anymore. Se we can't use `gorm.Model` here:

```go
import (
"time"

uuid "github.com/satori/go.uuid"
)

type User struct {
ID uuid.UUID `gorm:"primary_key;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Email string
}
```

Now if you try to create this model using a migration (or `CreateTable`) you'll
be faced with the issue that the `uuid-ossp` doesn't exist in your database and
is required. So the simple solution is to create this extension on the database
you're using:

```sql
> CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
```

Alright now you're good to go. But what if we had an initial database migration
that checked for that extension before creating tables and doing a lot of
other stuff?

Here's an example on how to do it:

```go
import (
"errors"

"github.com/jinzhu/gorm"
gormigrate "gopkg.in/gormigrate.v1"
)

var precheck = &gormigrate.Migration{
ID: "precheck",
Migrate: func(tx *gorm.DB) error {
var (
requiredext = "uuid-ossp"
count int
)
if err := tx.Table("pg_extension").Where("extname = ?", requiredext).Count(&count).Error; err != nil {
return err
}
if count < 1 {
return errors.New("extension uuid-ossp doesn't exist in the target database but is required")
}
return nil
},
}
```

Run this migration first, it will return an error this extension isn't found
thus preventing the following migrations to run.

# Troubleshooting

## Relations are not created using CreateTable
Expand Down
211 changes: 196 additions & 15 deletions pages/qor-admin-tips-and-tricks.md
@@ -1,10 +1,10 @@
title: "QOR Admin: Tips and Tricks"
title: "QOR Admin Tutorial"
description: |
QOR Admin is a great admin interface creation tool but, just like gorm,
the documentation sometimes lacks explanation and tutorials to help you get
started
slug: qor-admin-tips-and-tricks
banner: "/assets/dialogflow/banner.png"
slug: qor-admin-tutorial
banner: "/assets/qor-admin/banner.png"
draft: true
date: 2019-02-11 17:30:00
tags: [go,dev,admin,qor]
Expand All @@ -13,7 +13,7 @@ tags: [go,dev,admin,qor]

## What is QOR?

> QOR is a set of libraries written in Go that abstracts common features needed
> QOR is a set of libraries written in Go that abstracts common features needed
> for business applications, CMSs, and E-commerce systems.
>
> <cite>[QOR GitHub Repository](https://github.com/qor/qor)</cite>
Expand All @@ -23,7 +23,7 @@ interests us here is the admin interface. Let's say we're creating an API
and store our data (stuff like your users, products, etc...) in a database. You
just don't need QOR to do that, you can use whatever router you're used to work
with for example. But QOR is a full framework, if you want to look at everything
it can do, head to the [getqor website](https://getqor.com/) and prepare
it can do, head to the [getqor website](https://getqor.com/) and prepare
yourself to be amazed because it can do so many things:

![qor features](/assets/qor-admin/qor-features.png)
Expand All @@ -43,9 +43,9 @@ the usual SQL query to export data, if you know what I mean.

It also enables to modify data and more importantly **keep it consistent** by
writing your business rules as part of your admin interface. So we can prevent
someone from modifying one of our products and set the price to $0 (or 0€, or
someone from modifying one of our products and set the price to \$0 (or 0€, or
whatever the currency, you get my point). Or prevent data loss. Or set specific
behavior for certain fields. The admin interface use case is then completely
behavior for certain fields. The admin interface use case is then completely
different of a dashboard that does only "read" operations to generate insights
like [Metabase](https://www.metabase.com/) (which is an amazing tool too!).

Expand All @@ -61,21 +61,202 @@ following screenshot is from the [QOR Admin Demo](http://demo.getqor.com/admin):
Although QOR Admin is an amazing open-source lib and product, sometimes the
documentation lacks of a clear way to do things. This article's goal is to act
as a kind of enhanced documentation and tutorial. We'll try to leverage the
annoying steps of setting up QOR following its best practices and create some
annoying steps of setting up QOR following its best practices and create some
kind of package that could be reused quickly without having to overthink things.

# First Steps
# Initial Setup

## QOR Admin and Gorm
## Heads Up

So QOR (in general and not just admin) is tightly coupled with
QOR (in general and not just admin) is tightly coupled with
[gorm](https://gorm.io), mostly because gorm is an amazing ORM for the Go
language, and also because [jinzhu](https://github.com/jinzhu) is an amazing
human being who created gorm and helped creating QOR.
language, and also because [jinzhu](https://github.com/jinzhu) who created gorm
also helped creating QOR. The main issue here is that if you're not already
using gorm, you might have a hard time using it "only" for the admin interface.
For this reason we won't cover the case where you're not already using gorm for
your API or web service.

## QOR Admin and Gin
## Base Program

## Authentication
Let's get started by creating a base program which we'll then use as a reference
for our future QOR related use.

```go
package main

import (
"time"

"github.com/jinzhu/gorm"
uuid "github.com/satori/go.uuid"
"github.com/sirupsen/logrus"
)

type product struct {
ID uuid.UUID `gorm:"primary_key;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time

Name string
Price int
}

func main() {
db, err := gorm.Open("postgres", "host=...")
if err != nil {
logrus.WithError(err).Fatal("Couldn't initialize database connection")
}
defer db.Close()
}
```

This is a minimal example, this post isn't about gorm itself but about QOR
so we'll keep things simple like that. If you need more information on how to
use gorm, please refer to my [Gorm Gotchas](/post/gorm-gotchas) post.

# Authentication

Now we have a nice looking admin interface which integrates our model and allows
us to modify data in our database. But there's no authentication in front of it,
which is seriously problematic. Luckily, after a quick search on
[QOR's Documentation](https://doc.getqor.com/) we found a page about
authentication [here](https://doc.getqor.com/admin/authentication.html)!

We're told that we need to implement an interface. Well ok. How are we supposed
to do that? Well we need to understand how a simple auth system works, cookies
forms and other stuff like that. So let's get started.

## Admin User Model

The goal here is to create another table which will only contain our admin users
for our admin interface (people that are allowed to login and do things).
We don't need a really complex one, just enough to identify people connecting
on our admin interface, so let's go with something like that:

```go
// AdminUser defines how an admin user is represented in database
type AdminUser struct {
gorm.Model
Email string `gorm:"not null;unique"`
FirstName string
LastName string
Password []byte
LastLogin *time.Time
}
```

Basic really. Email, name, password and last login operation. Security people
I see you, don't worry, the password won't be clear text. We're not barbarians.

So we're going to add a few methods:

```go
// DisplayName satisfies the interface for Qor Admin
func (u AdminUser) DisplayName() string {
if u.FirstName != "" && u.LastName != "" {
return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
}
return u.Email
}

// HashPassword is a simple utility function to hash the password sent via API
// before inserting it in database
func (u *AdminUser) HashPassword() error {
pwd, err := bcrypt.GenerateFromPassword(u.Password, bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = pwd
return nil
}

// CheckPassword is a simple utility function to check the password given as raw
// against the user's hashed password
func (u AdminUser) CheckPassword(raw string) bool {
return bcrypt.CompareHashAndPassword(u.Password, []byte(raw)) == nil
}
```

That's right let's just use bcrypt. Now we need to create that table and add our
first user.

## Migration

If you read my previous post about gorm, you might know that I'm using a package
called [gormigrate](https://github.com/go-gormigrate/gormigrate) to handle
database migrations. This package is really useful because it allows to run
migrations and more importantly, to rollback if they fail. As I wrote in my
previous article, embedding your model inside the migration ensures the
migrations can be run in the right order even if you modify the main `AdminUser`
model. So let's create our table, and immediately add our first admin user
with its email address and a `changeme` password.

```go
import (
"time"

"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt"
gormigrate "gopkg.in/gormigrate.v1"
)

var initAdmin = &gormigrate.Migration{
ID: "init_admin",
Migrate: func(tx *gorm.DB) error {
var err error

type adminUser struct {
gorm.Model
Email string `gorm:"not null;unique"`
FirstName string
LastName string
Password []byte
LastLogin *time.Time
}

if err = tx.CreateTable(&adminUser{}).Error; err != nil {
return err
}
var pwd []byte
if pwd, err = bcrypt.GenerateFromPassword([]byte("changeme"), bcrypt.DefaultCost); err != nil {
return err
}
usr := adminUser{
Email: "youremailaddress@yourcompany.com",
Password: pwd,
}
return tx.Save(&usr).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.DropTable("admin_users").Error
},
}
```

## Adding Admin User to Admin

If everything went well, you now have a new table called `admin_users` which
only contains one single record, the user you created in the migration. We're
going to add the `AdminUser` model to our admin interface **but** we need to
define the behavior intended for the `Password` field. QOR Admin is great but
if you don't tell it what to do with the data, it just puts it there. Meaning:
a clear text password if we modify our user in the admin interface and save it.

This is where QOR Admin can get tricky.

```go

```

# Deployment and Bindatafs

This part was tricky. As in, really tricky and it took me a lot of time to
actually understand what was going on.

# Thanks

- Gin-Gonic Framework Logo by
[Javier Provecho](https://github.com/javierprovecho) is licensed under a
[Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).
- Scientist Gopher by [marcusolsson](https://github.com/marcusolsson) from the [gophers repo](https://github.com/marcusolsson/gophers)

0 comments on commit 1c2f348

Please sign in to comment.