User Tools

Site Tools


Mini application pattern

goweb.go

goweb.go
/*
 * Copyright 2019 Oleg Borodin  <borodin@unix7.org>
 */
 
package main
 
import (
    "net/http"
    "log"
    "errors"
    "strings"
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
    "fmt"
    "os"
    "time"
    "github.com/dustin/go-humanize"
    "github.com/jmoiron/sqlx"
    _ "github.com/jackc/pgx/v4/stdlib"
    "goweb/accounts"
)
 
type DbInfo struct {
    DatName string      `db:"datname" json:"datname" xml:"datname"`
    Size int64          `db:"size" json:"size" xml:"size"`
    Owner string        `db:"owner" json:"owner" xml:"owner"`
    NumBackends int     `db:"numbackends" json:"numbackends" xml:"numbackends"`
    HumanSize string
}
 
type Env struct {
    db *sqlx.DB
}
 
 
func main() {
 
   db, err := sqlx.Open("pgx", "postgres://pgsql@localhost/postgres?sslmode=disable")
    if err != nil {
        fmt.Printf("error: %s\n", err)
        os.Exit(1)
    }
    defer db.Close()
    err = db.Ping()
    if err != nil {
        fmt.Printf("error: %s\n", err)
        os.Exit(1)
    }
 
    go func() {
        for {
            if err := db.Ping(); err != nil {
                log.Printf("db error: %s\n", err)
            }
            time.Sleep(2 * time.Second)
        }
    }()
 
    env := &Env{db: db}
 
    router := gin.Default()
    router.LoadHTMLGlob("./templates/*.html")
    store := cookie.NewStore([]byte("supersecret"))
    router.Use(sessions.Sessions("session", store))
 
    router.GET("/login", env.Login)
    router.GET("/logout", env.Logout)
    router.POST("/auth", env.Auth)
 
    authorized := router.Group("/")
    authorized.Use(CheckAuthMiddleware)
 
    authorized.GET("/", env.Home)
 
    authorized.GET("/users", env.Users)
    authorized.POST("/user/create", env.UserCreate)
    authorized.POST("/user/update", env.UserCreate)
    authorized.POST("/user/delete", env.UserDelete)
 
    router.Static("/assets", "./public/assets/")
    router.Run(":8080")
}
 
func CheckAuthMiddleware(context *gin.Context) {
    session := sessions.Default(context)
    username := session.Get("username")
    if username == nil {
        context.Redirect(http.StatusMovedPermanently, "/login")
        return
    }
    context.Next()
}
 
func userAuth(username string, password string) error {
    if len(strings.TrimSpace(username)) > 0 && len(strings.TrimSpace(username)) > 0 {
        return nil
    }
    return errors.New("username or password mismatch")
}
 
func (env *Env) Login(context *gin.Context) {
    session := sessions.Default(context)
    session.Delete("username")
    session.Save()
    context.HTML(http.StatusOK, "login.html", nil)
}
 
func (env *Env) Logout(context *gin.Context) {
    context.Redirect(http.StatusMovedPermanently, "/login")
}
 
 
func (env *Env) DbInfo(context *gin.Context) {
 
    query := `select d.datname as datname,
                pg_database_size(d.datname) as size,
                u.usename as owner,
                s.numbackends as numbackends
            from pg_database d, pg_user u, pg_stat_database s
            where d.datdba = u.usesysid and d.datname = s.datname
            order by d.datname`
 
    var dbis []DbInfo
    _ = env.db.Select(&dbis, query)
 
    for item := range dbis {
        dbis[item].HumanSize =  humanize.Comma(dbis[item].Size)
    }
 
    context.HTML(http.StatusOK, "home.html", &dbis)
}
 
func (env *Env) Home(context *gin.Context) {
    context.Redirect(http.StatusMovedPermanently, "/users")
 
}
 
type User struct {
    Username string `form:"username" json:"username" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}
 
func (env *Env) Auth(context *gin.Context) {
    var user User
    if err := context.ShouldBind(&user); err != nil {
        log.Println("auth: error binding")
        context.Redirect(http.StatusMovedPermanently, "/login")
    }
    if err := userAuth(user.Username, user.Password); err == nil {
        session := sessions.Default(context)
        session.Set("username", user.Username)
        session.Save()
        context.Redirect(http.StatusMovedPermanently, "/")
        return
    }
    context.Redirect(http.StatusMovedPermanently, "/login")
}
 
func (env *Env) Users(context *gin.Context) {
    aDB := accounts.New("passwords")
    list, _ := aDB.List()
    context.HTML(http.StatusOK, "users.html", &list)
}
 
func (env *Env) UserCreate(context *gin.Context) {
    var user User
    if err := context.ShouldBind(&user); err != nil {
        log.Println("user create: error binding")
        context.Redirect(http.StatusMovedPermanently, "/users")
    }
    aDB := accounts.New("passwords")
    aDB.Set(user.Username, user.Password)
 
    context.Redirect(http.StatusMovedPermanently, "/users")
}
 
func (env *Env) UserUpdate(context *gin.Context) {
    var user User
    if err := context.ShouldBind(&user); err != nil {
        log.Println("user create: error binding")
        context.Redirect(http.StatusMovedPermanently, "/users")
    }
    aDB := accounts.New("passwords")
    aDB.Set(user.Username, user.Password)
 
    context.Redirect(http.StatusMovedPermanently, "/users")
}
 
func (env *Env) UserDelete(context *gin.Context) {
    var user User
    if err := context.ShouldBind(&user); err != nil {
        log.Println("user create: error binding")
        context.Redirect(http.StatusMovedPermanently, "/users")
    }
    aDB := accounts.New("passwords")
    aDB.Delete(user.Username)
 
    context.Redirect(http.StatusMovedPermanently, "/users")
}

accounts/accounts.go

accounts/accounts.go
/*
 * Copyright 2019 Oleg Borodin  <borodin@unix7.org>
 */
 
package accounts
 
import (
    "math/rand"
    "fmt"
    "os"
    "bufio"
    "regexp"
    "errors"
    "github.com/GehirnInc/crypt"
    _ "github.com/GehirnInc/crypt/sha256_crypt"
)
 
type AccountDB struct {
    FileName string
}
 
type Account struct{
    Username string
    Digest string
}
 
 
var passwordMinLen int = 4
var nameMinLen int = 4
 
const letters = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 
func randString(n int) string {
    arr := make([]byte, n)
    for i := range arr {
        arr[i] = letters[rand.Intn(len(letters))]
    }
    return string(arr)
}
 
func createHash(key string) (string, error) {
    crypt := crypt.SHA256.New()
    return crypt.Generate([]byte(key), []byte("$5$" + randString(12)))
}
 
func validateUsername(name string) error {
    if len(name) < nameMinLen {
        return errors.New(fmt.Sprintf("name less %d chars", nameMinLen))
    }
    return nil
}
 
func validatePassword(password string) error {
    if len(password) < passwordMinLen {
        return errors.New(fmt.Sprintf("password less %d chars", passwordMinLen))
    }
    return nil
}
 
func validateFileName(name string) error {
    if len(name) < 1 {
        return errors.New(fmt.Sprintf("filename not set"))
    }
    return nil
}
 
func (accountDB AccountDB) Set(username string, password string) error {
 
    if err := validateFileName(accountDB.FileName); err != nil {
        return err
    }
    if err := validateUsername(username); err != nil {
        return err
    }
    if err := validatePassword(password); err != nil {
        return err
    }
 
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY|os.O_CREATE, 0640)
    if err != nil {
        return err
    }
    scanner := bufio.NewScanner(file)
 
    digest, _ := createHash(password)
    accounts := map[string]string{username: string(digest)}
    re := regexp.MustCompile(`(.+):(.+)`)
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
            name := match[1]
            digest := match[2]
            if _, exists :=  accounts[name]; !exists {
                accounts[name] = digest
            }
        }
    }
    file.Close()
 
    _ = os.Rename(accountDB.FileName, accountDB.FileName + ".bak")
 
    file, err = os.OpenFile(accountDB.FileName, os.O_WRONLY|os.O_CREATE, 0640)
    if err != nil {
        return err
    }
 
    for key, value := range accounts {
        file.Write([]byte(fmt.Sprintf("%s:%s\n", key, value)))
    }
    file.Close()
    return nil
}
 
func (accountDB AccountDB) Print() error {
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    defer file.Close()
    if err != nil {
        return err
    }
 
    scanner := bufio.NewScanner(file)
    re := regexp.MustCompile(`^(.+):(.+)`)
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
            fmt.Println(match[1], match[2])
        }
    }
    return nil
}
 
func (accountDB AccountDB) List() ([]Account, error) {
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    defer file.Close()
    if err != nil {
        return nil, err
    }
 
    scanner := bufio.NewScanner(file)
    re := regexp.MustCompile(`^(.+):(.+)`)
 
    var accountList []Account
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
 
            account := Account{
                Username: match[1],
                Digest: match[2],
            }
            accountList = append(accountList, account)
        }
    }
    return accountList, nil
}
 
func (accountDB AccountDB) Auth(username string, password string) error {
 
    if err := validateFileName(accountDB.FileName); err != nil {
        return err
    }
 
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    defer file.Close()
    if err != nil {
        return err
    }
    scanner := bufio.NewScanner(file)
 
    re := regexp.MustCompile(`^(.+):(.+)`)
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            crypt := crypt.SHA256.New()
            match := re.FindStringSubmatch(str)
            if match[1] == username {
                return crypt.Verify(match[2], []byte(password))
            }
        }
    }
    return errors.New(fmt.Sprintf("account %s not found", username))
}
 
func (accountDB AccountDB) Delete(username string) error {
 
    if err := validateFileName(accountDB.FileName); err != nil {
        return err
    }
 
    file, err := os.OpenFile(accountDB.FileName, os.O_RDONLY, 0640)
    if err != nil {
        return err
    }
    scanner := bufio.NewScanner(file)
 
    re := regexp.MustCompile(`(.+):(.+)`)
 
    accounts := map[string]string{}
 
    for scanner.Scan() {
        str := scanner.Text()
        if re.MatchString(str) {
            match := re.FindStringSubmatch(str)
            xname := match[1]
            xdigest := match[2]
            if xname != username {
                if _, exists :=  accounts[xname]; !exists {
                    accounts[xname] = xdigest
                }
            }
        }
    }
    file.Close()
 
    //err = os.Remove(accountDB.FileName)
    err = os.Rename(accountDB.FileName, accountDB.FileName + ".bak")
    if err != nil {
        return err
    }
 
    file, err = os.OpenFile(accountDB.FileName, os.O_WRONLY|os.O_CREATE, 0640)
    if err != nil {
        return err
    }
    for xname, xdigest := range accounts {
        //fmt.Println("write:", xname, xdigest)
        file.Write([]byte(fmt.Sprintf("%s:%s\n", xname, xdigest)))
    }
    file.Close()
    return nil
}
 
func New(filename string) *AccountDB {
    return &AccountDB{ FileName: filename }
}

templates/footer.html

templates/footer.html
    </div>
 
    <div class="container">
        <div class="row">
            <div class="col mt-5">
                <hr class="justify-content-sm-center" />
                <div class="text-center">
                    <small>made by <a href="http://wiki.unix7.org">oleg borodin</a></small>
                </div>
            </div>
        </div>
    </div>
    <script src="/assets/bundle.js"></script>
</body>
 
</html>

templates/header.html

templates/header.html
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
 
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="/assets/bundle.css" rel="stylesheet">
 
    <title>G2</title>
 
</head>
 
<body>
 
    <nav class="navbar navbar-expand-sm sticky-top navbar-dark bg-dark mb-4">
 
        <div class="navbar-brand">
            <i class="fab fa-old-republic fa-lg"></i> G2
        </div>
 
        <ul class="nav justify-content-end ml-auto mr-3">
 
            <li class="nav-item">
                    <a href="/logout"><i class="fas fa-sign-out-alt fa-lg"></i></a>
            </li>
 
        </ul>
    </nav>
 
    <div class="container-fluid">

templates/login.html

templates/login.html
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
    <link href="/assets/bundle.css" rel="stylesheet">
 
    <title>G2 Login</title>
 
</head>
 
<body>
 
    <div>
        <nav class="navbar navbar-expand-sm sticky-top navbar-dark bg-dark mb-4">
            <span class="navbar-text ml-3">
                <i class="fab fa-old-republic fa-lg"></i>
                G2 Login
            </span>
 
            <ul class="nav justify-content-end ml-auto">
 
 
            </ul>
        </nav>
 
        <div class="container">
            <div class="row justify-content-center">
                <div class=" col-8 col-sm-6 col-md-4 border p-4 mt-sm-5 ml-3 mr-3">
 
                    <form action="/auth" accept-charset="UTF-8" method="post">
 
                        <div class=" form-group">
                            <label for="username">Username</label>
                            <input type="text" name="username" id="username" class="form-control" />
                        </div>
 
                        <div class="form-group">
                            <label for="password">Password</label>
                            <input type="password" name="password" id="password" class="form-control" />
                        </div>
 
                        <div class="text-center">
                            <button name="button" type="submit" class="btn btn-primary btn-sm">Submit</button>
                        </div>
 
                    </form>
 
                </div>
            </div>
        </div>
 
        <div class="container">
            <div class="row">
                <div class="col mt-5">
                    <hr class="justify-content-sm-center" />
                    <div class="text-center">
                        <small><a href="http://wiki.unix7.org"></a></small>
                    </div>
                </div>
            </div>
        </div>
    <script src="/assets/bundle.js"></script>
 
</body>
 
</html>

templates/users.html

templates/users.html
{{ template "header.html" . }}
 
<div class="modal fade" id="user-create" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
 
            <form method="post" action="/user/create">
 
                <div class="modal-header">
                    <h5 class="modal-title">Create user</h5>
                    <button type="button" class="close" data-dismiss="modal">
                        <span>&times;</span>
                    </button>
                </div>
 
                <div class="modal-body">
                    <div class="form-group">
                        <label for="username">Username:</label>
                        <input type="text" class="form-control" name="username" id="username">
                    </div>
 
                    <div class="form-group">
                        <label for="password">Password:</label>
                        <input type="password" class="form-control" name="password" id="password">
                    </div>
 
                </div>
 
                <div class="modal-footer">
                    <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">Close</button>
                    <button type="submit" class="btn btn-sm btn-primary">Create</button>
                </div>
            </form>
 
        </div>
    </div>
</div>
 
 
<div class="container">
    <div class="row justify-content-center">
        <div class="col-10 col-sm-12 col-md-8 mt-sm-3">
 
            <div class="row">
                <div class="col">
                    <div class="btn btn-primary btn-sm ml-auto float-right mb-3 d-inline-block" data-toggle="modal" data-target="#user-create">
                        <i class="fas fa-plus"></i>
                    </div>
                    <h5 class="mb-3"><b>Users</b></h5>
                </div>
            </div>
 
            <table class="table table-striped table-hover table-sm">
 
                <thead class="thead-light">
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">username</th>
                        <th scope="col"><div class="btn btn-sm btn-light"><i class="far fa-edit"></i></div></th>
                        <th scope="col"><div class="btn btn-sm btn-light"><i class="far fa-trash-alt"></i></div></th>
                    </tr>
                </thead>
 
                <tbody>
 
                    {{ range $i, $v := . }}
 
                    <tr>
 
                        <td>{{ $i }}</td>
                        <td>{{ $v.Username }}</td>
 
                        <td>
 
                            <div class="modal fade" id="user-update-{{ $i }}" tabindex="-1" role="dialog">
                                <div class="modal-dialog" role="document">
                                    <div class="modal-content">
 
                                        <form method="post" action="/user/update">
 
                                            <div class="modal-header">
                                                <h5 class="modal-title">Update {{ $v.Username }}</h5>
                                                <button type="button" class="close" data-dismiss="modal">
                                                    <span>&times;</span>
                                                </button>
                                            </div>
 
                                            <div class="modal-body">
 
                                                <input type="hidden" name="username" value="{{ $v.Username }}" />
 
                                                <div class="form-group">
                                                    <label for="password">New Password:</label>
                                                    <input type="password" class="form-control" name="password" id="password">
                                                </div>
 
                                            </div>
 
                                            <div class="modal-footer">
                                                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">Close</button>
                                                <button type="submit" class="btn btn-sm btn-primary">Update</button>
                                            </div>
                                        </form>
                                    </div>
                                </div>
                            </div>
 
                            <div class="btn btn-sm btn-light" data-toggle="modal" data-target="#user-update-{{ $i }}">
                                <i class="far fa-edit"></i>
                            </div>
 
                        </td>
 
                        <td>
                            <div class="modal fade" id="user-delete-{{ $i }}" tabindex="-1" role="dialog">
                                <div class="modal-dialog" role="document">
                                    <div class="modal-content">
 
                                        <form method="post" action="/user/delete">
 
                                            <div class="modal-header">
                                                <h5 class="modal-title">Drop {{ $v.Username }}</h5>
                                                <button type="button" class="close" data-dismiss="modal">
                                                    <span>&times;</span>
                                                </button>
                                            </div>
 
                                            <div class="modal-body">
 
                                                <input type="hidden" name="username" value="{{ $v.Username }}" />
                                                <input type="hidden" name="password" value="dummy" />
 
                                                <div class="form-check">
                                                    <input type="checkbox" class="form-check-input" id="confirmation">
                                                    <label class="form-check-label" for="confirmation"> I agree to this action</label>
                                                </div>
 
                                            </div>
 
                                            <div class="modal-footer">
                                                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">Close</button>
                                                <button type="submit" class="btn btn-sm btn-primary">Drop</button>
                                            </div>
                                        </form>
 
                                    </div>
                                </div>
                            </div>
 
                            <div class="btn btn-sm btn-light" data-toggle="modal" data-target="#user-delete-{{ $i }}">
                                <i class="far fa-trash-alt"></i>
                            </div>
 
                        </td>
 
                    </tr>
 
                    {{ end }}
 
                </tbody>
 
            </table>
 
        </div>
    </div>
</div>
 
{{ template "footer.html" . }}