/* * Author, Copyright: Oleg Borodin <onborodin@gmail.com> */ 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") }
/* * Author, Copyright: Oleg Borodin <onborodin@gmail.com> */ 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 } }
</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>
<!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">
<!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>
{{ 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>×</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>×</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>×</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" . }}