OpenProject Community Kubernetes deployment file

deployment.yaml

---
apiVersion: v1
kind: Namespace
metadata:
  name: openproject
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openproject
  namespace: openproject
  labels:
    app: openproject
spec:
  replicas: 1
  selector:
    matchLabels:
      app: openproject
  template:
    metadata:
      labels:
        app: openproject
    spec:
      nodeSelector:
        app: openproject
      containers:
      - name: openproject
        image: openproject/community:10
        env:
          - name: SECRET_KEY_BASE
            value: secret
        ports:
        - containerPort: 8080
          name: openproject
          protocol: TCP
        livenessProbe:
          tcpSocket:
            port: 8080
          initialDelaySeconds: 600
          timeoutSeconds: 2
        readinessProbe:
          tcpSocket:
            port: 8080
          initialDelaySeconds: 600 # 10 minutes
          timeoutSeconds: 2
        volumeMounts:
        - name: pgdata
          mountPath: /var/openproject/pgdata
        - name: assets
          mountPath: /var/openproject/assets
      volumes:
        - name: pgdata
          hostPath:
            path: /opt/openproject/pgdata
            type: DirectoryOrCreate
        - name: assets
          hostPath:
            path: /opt/openproject/static
            type: DirectoryOrCreate

OpenProject Community 10

Username: admin
Password: admin

Reference: https://docs.openproject.org/installation-and-operations/installation/docker/

Error:
Hostname setting mismatch Your application is running with its host name setting set to localhost:3000, but the request is a “your hostname” hostname. This will result in errors! Go to System settings and change the “Host name” setting to correct this.

Solution:
Browse to System Settings (/settings). Change the value of the hostname to the correct hostname (as you define).

Example use of Jenkins REST API with Golang

Hi!

I’m going to describe how to call Jenkins REST APIs using Go. We will create three Go files:
1. packages/helpers/helpers.go
2. packages/jenkins/jenkins.go
3. main.go

The source codes are available at https://github.com/mohdnaim/jenkins_rest_api

helpers.go contains function helpers. We put functions inside this file to maintain clean and organized codes.

jenkins.go contains functions related to Jenkins. Such functions are IsJobExist() to check whether a Jenkins job or build pipeline exists, CopyJenkinsJob() to copy a Jenkins job and DownloadConfigXML() to download the configurations of a job in XML format.

main.go is our main Go file, the starting point of our program.

main.go

package main

import (
	"fmt"
	"log"
	"strings"

	helpers "./packages/helpers"
	jenkins "./packages/jenkins"
)

func main() {
	// compulsory to set
	jenkinsURL := "http://127.0.0.1/"
	jenkinsUsername := "put your username here"
	jenkinsAPIToken := "put your API token here"

	jenkins.JenkinsDetails = jenkins.Details{jenkinsURL, jenkinsUsername, jenkinsAPIToken}
	xmlFolder := "xml"

	// 1. get all existing projects / jenkins jobs
	allProjectNames := jenkins.GetAllProjectNames()

	// 2. filter out projects that we want
	filteredProjectNames := make([]string, 0)
	for _, projectName := range allProjectNames {
		// do something

		// append to another slice based on condition
		if strings.HasPrefix(projectName, "prefix") {
			filteredProjectNames = append(filteredProjectNames, projectName)
		}
	}

	// 3. for each project, get its config.xml
	for _, projectName := range filteredProjectNames {
		xmlPath := fmt.Sprintf("%s/%s.xml", xmlFolder, projectName)
		if err := jenkins.DownloadConfigXML(projectName, xmlPath); err != nil {
			log.Println("error download config.xml for project:", projectName)
			continue // skip
		}
	}

	// 4. modify its config.xml
	files := helpers.GetFilenamesRecursively(xmlFolder)
	for _, xmlFile := range files {
		log.Println(xmlFile)
	}

	// 4b. rewrite config.xml

	// 5. http request POST updated config.xml
	for _, xmlFile := range files {
		tmpSlice := strings.Split(xmlFile, "/")
		projectName := tmpSlice[len(tmpSlice)-1]
		log.Println(projectName)

		if err := jenkins.PostConfigXML(projectName, xmlFile); err != nil {
			log.Println("error postconfigxml:", projectName)
		}
	}
}

We need to configure the details of the Jenkins instance. So set correct values to the following variables:
jenkinsURL := “http://127.0.0.1/”
jenkinsUsername := “put your username here”
jenkinsAPIToken := “put your API token here”

Line #18 creates a struct containing the three variables above. The struct is used by functions in jenkins.go so before we invoke any function in the jenkins.go module, we need to set the values.

Line #19 sets the folder name we store the XML files – which are the configuration file of Jenkins jobs.

Line #22 – we retrieve the names of all projects exist in the Jenkins instance. If we look at the implementation of the function GetAllProjectNames() in jenkins.go, the function invokes DownloadFileToBytes(). It makes HTTP request to /api/json with the username and password in the request header. The endpoint returns names of project in JSON format. It then converts the JSON format into ‘result’ map. For every name in the map, append the name into ‘allProjectNames’ array.

Line #24 – We filter out projects that we want. We iterate over the allProjectNames array and if a project meets certain criteria, we append the project name into ‘filteredProjectNames’ array which we will use onwards instead of ‘allProjectNames’.

Line #35 – For each project name in ‘filteredProjectNames’, we get its configuration file i.e. the config.xml.

Line #45 – We call a function GetFilenamesRecursively() in helpers.go to read all the file names in ‘xmlFolder’

Line #50 – We do whatever we want with the config.xml file such as renaming the project name, adding build parameters, change permission and so on.
I write a script in Python instead of Go to manipulate the config.xml files because Python has more libraries than Go that can help us to manipulate strings and files.

Line #52 – Finally after we make the change at line #50, we submit the change by executing POST request to Jenkins.

helpers.go

package helpers

import (
	"os"
	"path/filepath"
)

// StringInSlice ...
func StringInSlice(a string, list []string) bool {
	for _, b := range list {
		if b == a {
			return true
		}
	}
	return false
}

// GetFilenamesRecursively ...
func GetFilenamesRecursively(path string) []string {
	var files []string
	err := filepath.Walk("xml", func(path string, info os.FileInfo, err error) error {
		files = append(files, path)
		return nil
	})
	if err != nil {
		panic(err)
	}
	return files
}

jenkins.go

package jenkins

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"reflect"
)

// Details ...
type Details struct {
	URL      string
	Username string
	APIToken string
}

// JenkinsDetails ...
var JenkinsDetails = Details{}

// IsJobExist ...
func IsJobExist(projectName string) bool {
	fullURL := fmt.Sprintf("%sjob/%s", JenkinsDetails.URL, projectName)
	request, err := http.NewRequest("GET", fullURL, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		log.Println("project doesn't exist:", projectName)
		return false
	}
	if resp.StatusCode == 200 {
		// log.Println("project exists:", projectName)
		return true
	}
	log.Println("project doesn't exist:", projectName)
	return false
}

// CopyJenkinsJob ...
func CopyJenkinsJob(srcJob string, dstJob string) error {
	fullURL := fmt.Sprintf("%s%s?name=%s&mode=copy&from=%s", JenkinsDetails.URL, "createItem", dstJob, srcJob)
	request, err := http.NewRequest("POST", fullURL, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	if resp.StatusCode == 200 {
		return nil
	}
	return fmt.Errorf("error copying job: %s", srcJob)
}

// DownloadConfigXML ...
func DownloadConfigXML(projectName string, dstFilename string) error {
	fullURL := fmt.Sprintf("%sjob/%s/config.xml", JenkinsDetails.URL, projectName)
	if DownloadFile(fullURL, dstFilename) == nil {
		return nil
	}
	return fmt.Errorf("error downloading file %s", fullURL)
}

// PostConfigXML ...
func PostConfigXML(projectName string, filename string) error {
	// read content of file
	data, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
		return err
	}

	fullURL := fmt.Sprintf("%sjob/%s/config.xml", JenkinsDetails.URL, projectName)
	request, err := http.NewRequest("POST", fullURL, data)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}

	// perform the request
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	if resp.StatusCode == 200 {
		// log.Printf("success postConfigXML: %s %s %s", projectName, filename, resp.Status)
		return nil
	}
	return fmt.Errorf("error postConfigXML: %s %s %s", projectName, filename, resp.Status)
}

// DownloadFile ...
func DownloadFile(url string, filepath string) error {
	// Create the file
	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()

	// Get the data
	request, err := http.NewRequest("GET", url, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Write the body to file
	_, err = io.Copy(out, resp.Body)
	if err != nil {
		return err
	}

	return nil
}

// DownloadFileToBytes ...
func DownloadFileToBytes(url string) ([]byte, error) {
	request, err := http.NewRequest("GET", url, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err) // writes to standard error
	}
	return bodyBytes, nil
}

// GetAllProjectNames ...
func GetAllProjectNames() []string {
	allProjectNames := make([]string, 0)
	allProjectsURL := fmt.Sprintf("%sapi/json?pretty=true", JenkinsDetails.URL)
	if respBytes, err := DownloadFileToBytes(allProjectsURL); err == nil {
		// json --> map
		var result map[string]interface{}
		json.Unmarshal([]byte(respBytes), &result)

		// if map has slice 'jobs', iterate over it
		if jobs, keyIsPresent := result["jobs"]; keyIsPresent && reflect.TypeOf(jobs).Kind() == reflect.Slice {
			jobs2 := result["jobs"].([]interface{})
			for _, job := range jobs2 {
				// log.Println(job)
				_map, _ := job.(map[string]interface{}) // assert to map
				allProjectNames = append(allProjectNames, _map["name"].(string))
			}
		}
	}
	return allProjectNames
}

Golang binary search implementation

package main

import (
	"fmt"
	"math"
)

func binarySearch(arr []int, val int) int {
	var first = 0
	var last = len(arr) - 1
	for first <= last {
		var midpoint = int(math.Floor(float64((first + last) / 2)))
		if arr[midpoint] == val {
			return midpoint
		}
		if val < arr[midpoint] {
			last = midpoint - 1
		} else {
			first = midpoint + 1
		}
	}
	return -1
}

func main() {
	arr := []int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39}
	fmt.Println(binarySearch(arr, 35))
}

Golang queue data structure implementation

package main

import (
	"container/list"
	"errors"
	"fmt"
)

// Queue is a list data structure that follows First-In-First-Out (FIFO) methodology
type Queue struct {
	size  int       // size of the queue
	items list.List // holds the elements
}

// enqueue() method inserts new element at the back of the queue
func (queue *Queue) enqueue(str string) (bool, error) {
	if queue.items.Len() >= queue.size {
		return false, errors.New("Overflow")
	}
	queue.items.PushBack(str)
	return true, nil
}

// dequeue() method removes element at the front of the queue
func (queue *Queue) dequeue() (string, error) {
	if queue.items.Len() == 0 {
		return "", errors.New("Empty")
	}
	ret := queue.items.Front().Value.(string)
	queue.items.Remove(queue.items.Front())
	return ret, nil
}

func main() {
	queue := &Queue{size: 5}

	fmt.Println(queue.enqueue("1"))
	fmt.Println(queue.dequeue())
	fmt.Println(queue.dequeue())

	fmt.Println(queue.enqueue("1"))
	fmt.Println(queue.enqueue("2"))
	fmt.Println(queue.enqueue("3"))
	fmt.Println(queue.enqueue("4"))
	fmt.Println(queue.enqueue("5"))
	fmt.Println(queue.enqueue("6"))
}

Golang stack implementation

package main

import (
	"container/list"
	"errors"
	"fmt"
)

// Stack is a list that can only perform two operations: push and pop
type Stack struct {
	items list.List
}

// push() method inserts new element at the top of the stack
func (stack *Stack) push(str string) {
	stack.items.PushBack(str)
}

// pop() method removes the element at the top of the stack
func (stack *Stack) pop() (string, error) {
	if stack.items.Back() == nil {
		return "", errors.New("Empty")
	}
	lastVal := stack.items.Back().Value.(string) // get the value of last element
	stack.items.Remove(stack.items.Back())       // remove the last element
	return lastVal, nil
}

func main() {
	stack := &Stack{}
	stack.push("1")
	stack.push("2")
	fmt.Println(stack.pop())
	fmt.Println(stack.pop())
	fmt.Println(stack.pop())
}

Golang singly linked list

package main

import "fmt"

// Node can store two values, 'id' and 'name'.
// Another value, 'ptr' is a pointer to another node
type Node struct {
	id   int
	name string
	ptr  *Node
}

// LinkedList struct
type LinkedList struct {
	head *Node
}

// LinkedList method to append a node to tail
func (linkedlist *LinkedList) append(newnode *Node) *LinkedList {
	if linkedlist.head == nil {
		linkedlist.head = newnode
		newnode.ptr = nil
		return linkedlist
	} // else
	// initialization; condition; increment
	for n := linkedlist.head; n != nil; n = n.ptr {
		if n.ptr == nil {
			n.ptr = newnode
			return linkedlist
		}
	}
	return linkedlist
}

// LinkedList method to print the nodes details
func (linkedlist *LinkedList) print() {
	for n := linkedlist.head; n != nil; n = n.ptr {
		fmt.Println(n.id, n.name, n.ptr)
	}
}

// LinkedList method to insert a new node before a particular node
func (linkedlist *LinkedList) insertBefore(name string, newNode *Node) bool {
	if linkedlist.head == nil {
		return false
	}

	ptrNode := linkedlist.head // points to first node
	for ptrNode.ptr != nil {   // while() loop
		if ptrNode.ptr.name == name {
			newNode.ptr = ptrNode.ptr // new node points to the next node
			ptrNode.ptr = newNode     // current node points to the new node
			return true
		}
		ptrNode = ptrNode.ptr // points to next node
	}
	return false
}

func main() {
	// create a linked list object
	linkedlist := &LinkedList{}

	// create nodes
	nodeA := &Node{1, "A", nil}
	nodeB := &Node{2, "B", nil}
	nodeC := &Node{3, "C", nil}
	nodeD := &Node{4, "D", nil}
	nodeE := &Node{5, "E", nil}

	// append the nodes to the linked list
	linkedlist.append(nodeA)
	linkedlist.append(nodeB)
	linkedlist.append(nodeC)
	linkedlist.append(nodeD)
	linkedlist.append(nodeE)

	// print the linked list
	linkedlist.print()

	// create a new node
	nodeF := &Node{6, "F", nil}
	// insert the new node before node "B"
	linkedlist.insertBefore("B", nodeF)
	// print the linked list
	linkedlist.print()
}

Export multi-line environment variable, replace a variable in file with the former using Go

There are times you want to replace a string in a file with a multi-line environment variable.

For example, you create a Jenkins pipeline and have a multi-line / text build parameter. You want to replace a string in file with the multi-line build parameter.

Using Linux utility such as sed and awk would be complicated because you need to escape some characters in the multi-line environment variable such as ‘=’, ‘/’, ‘\’ and so on.

There are reasons why Python 2.7 is installed by default in Linux distros like Ubuntu and RHEL, and there are reasons why sysadmins prefer to use Go instead of Python.

In this article I show an example how to replace a string with the multi-line environment variable from Jenkins using Go.

1. Write the multi-line environment variable to a file

#!/bin/bash
echo -e "$my_variable" > my_variable

2. Create a Go script to replace a string with the variable

package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"strings"
)

func main() {

}

3. Write codes to parse command line arguments

	var filepath, replace, sSourceTextFile string
	flag.StringVar(&filepath, "file", "myfile.txt", "source file containing text to replace")
	flag.StringVar(&replace, "replace", "foo", "string to replace")
	flag.StringVar(&source, "source", "variable", "source file containing multi-line string/text to be replaced with")
	flag.Parse()

The codes above add command line arguments to the Go script.
The first argument is filepath, which points to the file path having the string that we want to replace
The second argument is the string that we want to replace
The third argument is the file containing the multi-line environment variable that we want to replace the string with

4. Read the file into memory

	// read the source file into memory
	input, err := ioutil.ReadFile(filepath)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

5. Read the file containing multi-line environment variable into memory

	// read the source text file into memory
	text, err := ioutil.ReadFile(source)
	if err != nil {
		fmt.Print(err)
		os.Exit(1)
	}

6. Perform the string replacement

	output := bytes.Replace(input, []byte(replace), []byte(text), -1)

7. Write the output into the same file

	if err = ioutil.WriteFile(filepath, output, 0666); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

8. Compile the codes
go build script-name.go

9. Run/test
./script-name -filepath=myfile.txt -replace=replaceme -source=my_variable

Implementation of AES encryption in Go

encryption.go

package helper

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io"
)

// hashTo32Bytes will compute a cryptographically useful hash of the input string.
func hashTo32Bytes(input string) []byte {

	data := sha256.Sum256([]byte(input))
	return data[0:]

}

// DecryptString takes two strings, cryptoText and keyString.
// cryptoText is the text to be decrypted and the keyString is the key to use for the decryption.
// The function will output the resulting plain text string with an error variable.
func DecryptString(cryptoText string, keyString string) (plainTextString string, err error) {

	encrypted, err := base64.URLEncoding.DecodeString(cryptoText)
	if err != nil {
		return "", err
	}
	if len(encrypted) < aes.BlockSize {
		return "", fmt.Errorf("cipherText too short. It decodes to %v bytes but the minimum length is 16", len(encrypted))
	}

	decrypted, err := DecryptAES(hashTo32Bytes(keyString), encrypted)
	if err != nil {
		return "", err
	}

	return string(decrypted), nil
}

// DecryptAES ...
func DecryptAES(key, data []byte) ([]byte, error) {
	// split the input up in to the IV seed and then the actual encrypted data.
	iv := data[:aes.BlockSize]
	data = data[aes.BlockSize:]

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	stream := cipher.NewCFBDecrypter(block, iv)

	stream.XORKeyStream(data, data)
	return data, nil
}

// EncryptString takes two string, plainText and keyString.
// plainText is the text that needs to be encrypted by keyString.
// The function will output the resulting crypto text and an error variable.
func EncryptString(plainText string, keyString string) (cipherTextString string, err error) {

	key := hashTo32Bytes(keyString)
	encrypted, err := encryptAES(key, []byte(plainText))
	if err != nil {
		return "", err
	}

	return base64.URLEncoding.EncodeToString(encrypted), nil
}

func encryptAES(key, data []byte) ([]byte, error) {

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	// create two 'windows' in to the output slice.
	output := make([]byte, aes.BlockSize+len(data))
	iv := output[:aes.BlockSize]
	encrypted := output[aes.BlockSize:]

	// populate the IV slice with random data.
	if _, err = io.ReadFull(rand.Reader, iv); err != nil {
		return nil, err
	}

	stream := cipher.NewCFBEncrypter(block, iv)

	// note that encrypted is still a window in to the output slice
	stream.XORKeyStream(encrypted, data)
	return output, nil
}

Echo framework + GORM = Blazing fast Golang server-side application. Authentication example

In this article I want to show example of login, logout and registration implementation using Go Echo framework and GORM (Go Object Relational Mapper) with PostgreSQL. We use JWT (JSON Web Token) to authenticate users.

Firstly we create main.go:

package main

import (
	"app1/helper"
	"app1/models"
	"fmt"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
	configuration := helper.GetConfig()

	gormParameters := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable", configuration.DbHost, configuration.DbPort, configuration.DbName, configuration.DbUsername, configuration.DbPassword)
	gormDB, err := gorm.Open("postgres", gormParameters)
	if err != nil {
		panic("failed to connect database")
	}
	helper.GormDB = gormDB

	// Migrate the schema (tables): User, Authentication
	helper.GormDB.AutoMigrate(&helper.User{})
	helper.GormDB.AutoMigrate(&helper.Authentication{})
	helper.GormDB.Model(&helper.Authentication{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE")

	echoFramework := echo.New()
	echoFramework.Use(middleware.Logger()) // log
	echoFramework.Use(middleware.CORS())   // CORS from Any Origin, Any Method

	echoGroupUseJWT := echoFramework.Group("/api/v1")
	echoGroupUseJWT.Use(middleware.JWT([]byte(configuration.EncryptionKey)))
	echoGroupNoJWT := echoFramework.Group("/api/v1")

	// /api/v1/users : logged in users
	echoGroupUseJWT.POST("/users/logout", models.Logout)

	// /api/v1/users : public accessible
	echoGroupNoJWT.POST("/users", models.CreateUser)
	echoGroupNoJWT.POST("/users/login", models.Login)

	defer helper.GormDB.Close()
	echoFramework.Logger.Fatal(echoFramework.Start(":1323"))
}

Secondly we create globals.go:

package helper

import (
	"regexp"
	"time"

	"github.com/jinzhu/gorm"
)

type Configuration struct {
	EncryptionKey string
	DbHost        string
	DbPort        string
	DbName        string
	DbUsername    string
	DbPassword    string
}

var configuration Configuration

type ModelBase struct {
	ID        int       `gorm:"primary_key"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}

type User struct {
	ModelBase        // replaces gorm.Model
	Email     string `gorm:"not null; unique"`
	Password  string `gorm:"not null" json:"-"`
	Name      string `gorm:"not null; type:varchar(100)"` // unique_index
}

type Authentication struct {
	ModelBase
	User   User `gorm:"foreignkey:UserID; not null"`
	UserID int
	Token  string `gorm:"type:varchar(200); not null"`
}


type CustomHTTPSuccess struct {
	Data string `json:"data"`
}

type ErrorType struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

type CustomHTTPError struct {
	Error ErrorType `json:"error"`
}

var GormDB *gorm.DB

func init() {
	configuration = Configuration{
		EncryptionKey: "F61L8L7CUCGN0NK6336I8TFP9Y2ZOS43",
		DbHost:        "localhost",
		DbPort:        "5432",
		DbName:        "postgres",
		DbUsername:    "postgres",
		DbPassword:    "postgres",
	}
}

func GetConfig() Configuration {
	return configuration
}

func ValidateEmail(email string) (matchedString bool) {
	re := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
	matchedString = re.MatchString(email)
	return
}

Finally our implementation of the authentication methods in user.go:

package models

import (
	"app1/helper"
	"fmt"
	"net/http"
	"strconv"

	jwt "github.com/dgrijalva/jwt-go"
	"github.com/labstack/echo"
)

func CreateUser(c echo.Context) error {
	m := echo.Map{}
	if err := c.Bind(&m); err != nil {
		// return err
	}
	name := m["name"].(string)
	email := m["email"].(string)
	password := m["password"].(string)
	confirmPassword := m["confirm_password"].(string)

	if password == "" || confirmPassword == "" || name == "" || email == "" {
		return echo.NewHTTPError(http.StatusBadRequest, "Please enter name, email and password")
	}

	if password != confirmPassword {
		return echo.NewHTTPError(http.StatusBadRequest, "Confirm password is not same to password provided")
	}

	if helper.ValidateEmail(email) == false {
		return echo.NewHTTPError(http.StatusBadRequest, "Please enter valid email")
	}

	if bCheckUserExists(email) == true {
		return echo.NewHTTPError(http.StatusBadRequest, "Email provided already exists")
	}

	configuration := helper.GetConfig()

	enc, _ := helper.EncryptString(password, configuration.EncryptionKey)

	user1 := helper.User{Name: name, Email: email, Password: enc}
	// globals.GormDB.NewRecord(user) // => returns `true` as primary key is blank
	helper.GormDB.Create(&user1)

	token := jwt.New(jwt.SigningMethodHS256)

	claims := token.Claims.(jwt.MapClaims)
	claims["name"] = user1.Name
	claims["email"] = user1.Email

	t, err := token.SignedString([]byte(configuration.EncryptionKey)) // "secret" >> EncryptionKey
	if err != nil {
		return err
	}

	authentication := helper.Authentication{}
	if helper.GormDB.First(&authentication, "user_id =?", user1.ID).RecordNotFound() {
		// insert
		helper.GormDB.Create(&helper.Authentication{User: user1, Token: t})
	} else {
		authentication.User = user1
		authentication.Token = t
		helper.GormDB.Save(&authentication)
	}

	return c.JSON(http.StatusOK, map[string]string{
		"token": t,
	})
}

func bCheckUserExists(email string) bool {
	user1 := helper.User{}
	if helper.GormDB.Where(&helper.User{Email: email}).First(&user1).RecordNotFound() {
		return false
	}
	return true
}

func ValidateUser(email, password string, c echo.Context) (bool, error) {
	fmt.Println("validate")
	var user1 helper.User
	if helper.GormDB.First(&user1, "email =?", email).RecordNotFound() {
		return false, nil
	}

	configuration := helper.GetConfig()

	decrypted, _ := helper.DecryptString(user1.Password, configuration.EncryptionKey)

	if password == decrypted {
		return true, nil
	}
	return false, nil
}

func Login(c echo.Context) error {
	m := echo.Map{}
	if err := c.Bind(&m); err != nil {
		// return err
	}
	email := m["email"].(string)
	password := m["password"].(string)

	var user1 helper.User
	if helper.GormDB.First(&user1, "email =?", email).RecordNotFound() {
		_error := helper.CustomHTTPError{
			Error: helper.ErrorType{
				Code:    http.StatusBadRequest,
				Message: "Invalid email & password",
			},
		}
		return c.JSONPretty(http.StatusBadGateway, _error, "  ")
	}

	configuration := helper.GetConfig()
	decrypted, _ := helper.DecryptString(user1.Password, configuration.EncryptionKey)

	if password == decrypted {
		token := jwt.New(jwt.SigningMethodHS256)

		claims := token.Claims.(jwt.MapClaims)
		claims["name"] = user1.Name
		claims["email"] = user1.Email
		claims["id"] = user1.ModelBase.ID

		t, err := token.SignedString([]byte(configuration.EncryptionKey)) // "secret" >> EncryptionKey
		if err != nil {
			return err
		}

		authentication := helper.Authentication{}
		if helper.GormDB.First(&authentication, "user_id =?", user1.ID).RecordNotFound() {
			// insert
			helper.GormDB.Create(&helper.Authentication{User: user1, Token: t})
		} else {
			// update
			authentication.User = user1
			authentication.Token = t
			helper.GormDB.Save(&authentication)
		}

		return c.JSON(http.StatusOK, map[string]string{
			"token": t,
		})
	} else {
		_error := helper.CustomHTTPError{
			Error: helper.ErrorType{
				Code:    http.StatusBadRequest,
				Message: "Invalid email & password",
			},
		}
		return c.JSONPretty(http.StatusBadGateway, _error, "  ")
	}
}

func Logout(c echo.Context) error {
	tokenRequester := c.Get("user").(*jwt.Token)
	claims := tokenRequester.Claims.(jwt.MapClaims)
	fRequesterID := claims["id"].(float64)
	iRequesterID := int(fRequesterID)
	sRequesterID := strconv.Itoa(iRequesterID)

	requester := helper.User{}
	if helper.GormDB.First(&requester, "id =?", sRequesterID).RecordNotFound() {
		return echo.ErrUnauthorized
	}

	authentication := helper.Authentication{}
	if helper.GormDB.First(&authentication, "user_id =?", requester.ModelBase.ID).RecordNotFound() {
		return echo.ErrUnauthorized
	}
	helper.GormDB.Delete(&authentication)
	return c.String(http.StatusAccepted, "")
}

 

Encryption and decryption functions are missing. Drop me an email to get them 🙂

Docker Commands for Beginners

Docker is similar to VMWare Workstation and Oracle VM VirtualBox. It enables host operating system (for example, Windows) to run guest operating system(s) on top of it.

Docker requires you to enable Hyper-V on Windows 10 Pro, while VirtualBox requires you to disable Hyper-V on Windows 10 Pro, so you cannot run both virtualization technology on the same OS.

Docker is widely preferred because of its efficiency in using computer resources.

  1. List all images
    docker images -a
  2. Remove an image
    docker rmi 
  3. List all containers
    docker ps -a
    docker container ls -a
  4. Pull an image
    docker pull :
    docker pull ubuntu:latest
  5. Run a container
    docker run -d -it --name  
    docker run -d -it --name myubuntu ubuntu
  6. Stop a container
    docker stop 
  7. Remove a container
    docker rm 
  8. Attach to a running container
    docker attach 
  9. Commit a container (save it as an image)
    docker commit  
  10. Execute command
    docker exec  
    docker exec myubuntu bash -c "ls -aF /home; cat /etc/passwd"
  11. Expose container ports to host
    docker run -it -d -p : 
    docker run -it -d -p 9100:9100 -p 9090:9090 --name mycentos centos