initial commit

This commit is contained in:
kandrusyak
2023-06-23 20:51:41 +03:00
parent 438dc1a716
commit 8e9eeb989c
13 changed files with 458 additions and 0 deletions

44
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,44 @@
image: docker:20.10.16
variables:
# When using dind service, you must instruct Docker to talk with
# the daemon started inside of the service. The daemon is available
# with a network connection instead of the default
# /var/run/docker.sock socket.
DOCKER_HOST: tcp://docker:2376
#
# The 'docker' hostname is the alias of the service container as described at
# https://docs.gitlab.com/ee/ci/services/#accessing-the-services.
# If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
# the variable must be set to tcp://localhost:2376 because of how the
# Kubernetes executor connects services to the job container
# DOCKER_HOST: tcp://localhost:2376
#
# Specify to Docker where to create the certificates. Docker
# creates them automatically on boot, and creates
# `/certs/client` to share between the service and job
# container, thanks to volume mount from config.toml
DOCKER_TLS_CERTDIR: "/certs"
# These are usually specified by the entrypoint, however the
# Kubernetes executor doesn't run entrypoints
# https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4125
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
services:
- docker:20.10.16-dind
stages:
- build
build-job:
stage: build
tags:
- docker
only:
- master
script:
- docker info
- docker build -t docker-registry.dopcore.com/andrusyakka/astra-users .
- echo "$DOCKER_PASSWORD" | docker login docker-registry.dopcore.com -u "$DOCKER_USER" --password-stdin
- docker push docker-registry.dopcore.com/andrusyakka/astra-users

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM kandrusyak/go-build as build
LABEL authors="andrusyakka@dopcore.com"
WORKDIR /app
ENV CGO_ENABLED 1
COPY go.mod ./
RUN go mod download
COPY . ./
RUN go build -o ./app
FROM alpine:edge
WORKDIR /
COPY --from=build /app /app
EXPOSE 8080
CMD ["/app/app"]

23
go.mod Normal file
View File

@@ -0,0 +1,23 @@
module astra-users
go 1.21rc1
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.3.0 // indirect
gorm.io/driver/sqlite v1.5.2 // indirect
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 // indirect
)

52
go.sum Normal file
View File

@@ -0,0 +1,52 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=

29
handler/auth.go Normal file
View File

@@ -0,0 +1,29 @@
package handler
import (
"astra-users/model"
"crypto/sha256"
"fmt"
"github.com/labstack/echo/v4"
"net/http"
)
func (h *Handler) Login(c echo.Context) (err error) {
// Bind
u := new(model.User)
if err = c.Bind(u); err != nil {
return
}
u.Password = fmt.Sprintf("%x", sha256.Sum256([]byte(u.Password)))
var t = h.DB.Where("user_name = ? AND password = ?", u.UserName, u.Password).First(&u)
if t.RowsAffected == 0 {
return c.NoContent(http.StatusNotFound)
}
u.Password = ""
return c.JSON(http.StatusOK, u)
}

7
handler/handler.go Normal file
View File

@@ -0,0 +1,7 @@
package handler
import "gorm.io/gorm"
type Handler struct {
DB *gorm.DB
}

14
handler/health.go Normal file
View File

@@ -0,0 +1,14 @@
package handler
import (
"github.com/labstack/echo/v4"
"net/http"
)
func (h *Handler) Health(c echo.Context) error {
type Resp struct {
Status string `json:"status"`
}
return c.JSON(http.StatusOK, Resp{Status: "OK"})
}

114
handler/users.go Normal file
View File

@@ -0,0 +1,114 @@
package handler
import (
"astra-users/model"
"astra-users/pkg/utils"
"crypto/sha256"
"fmt"
"github.com/labstack/echo/v4"
"net/http"
)
func (h *Handler) GetCurrentUser(c echo.Context) error {
userId := c.Request().Header.Get("UserId")
if userId == "" {
return c.NoContent(http.StatusBadRequest)
}
u := new(model.User)
tx := h.DB.First(&u, userId)
if tx.RowsAffected == 0 {
return c.NoContent(http.StatusNotFound)
}
u.Password = ""
return c.JSON(http.StatusOK, &u)
}
func (h *Handler) GetUsers(c echo.Context) error {
var users []model.User
query := c.Request().URL.Query()
limit, offset := utils.GetPaginationParams(query)
sortCol, isDesc, filterCol, filterVal := utils.GetFilterParams(query)
tx := h.DB.Limit(limit).Offset(offset)
utils.ApplyIdFilter(c, tx)
utils.ApplySort(tx, sortCol, isDesc)
utils.ApplyFilters(tx, filterCol, filterVal)
tx = tx.Find(&users)
if tx.RowsAffected == 1 {
return c.JSON(http.StatusOK, users[0])
}
return c.JSON(http.StatusOK, users)
}
func (h *Handler) UpdateUser(c echo.Context) (err error) {
var user = new(model.User)
var apiUser = new(model.APIUser)
tx := h.DB
id := c.Param("id")
tx = tx.Find(&user, id)
if tx.RowsAffected == 0 {
return c.NoContent(http.StatusNotFound)
}
if err = c.Bind(apiUser); err != nil {
return c.NoContent(http.StatusBadRequest)
}
user.FirstName = apiUser.FirstName
user.LastName = apiUser.LastName
tx = h.DB.Save(&user)
if tx.RowsAffected == 0 {
return c.NoContent(http.StatusBadGateway)
}
user.Password = ""
return c.JSON(http.StatusOK, &user)
}
func (h *Handler) DeleteUser(c echo.Context) (err error) {
id := c.Param("id")
tx := h.DB.Delete(&model.User{}, id)
if tx.RowsAffected == 0 {
return c.NoContent(http.StatusBadGateway)
}
return c.NoContent(http.StatusOK)
}
func (h *Handler) CreateUser(c echo.Context) (err error) {
var user = new(model.User)
if err = c.Bind(user); err != nil {
return c.NoContent(http.StatusBadRequest)
}
user.Password = fmt.Sprintf("%x", sha256.Sum256([]byte(user.Password)))
tx := h.DB.Omit("ID", "CreatedAt", "UpdatedAt", "DeletedAt").Create(user)
if tx.RowsAffected == 0 {
return c.NoContent(http.StatusBadGateway)
}
user.Password = ""
return c.JSON(http.StatusCreated, user)
}

52
main.go Normal file
View File

@@ -0,0 +1,52 @@
package main
import (
"astra-users/handler"
"astra-users/model"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
func main() {
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: false, // Disable color
},
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic("failed to connect database")
}
e := echo.New()
db.AutoMigrate(&model.User{})
e.Use(middleware.Logger())
var h = &handler.Handler{DB: db}
e.POST("/login", h.Login)
e.GET("/me", h.GetCurrentUser)
e.POST("/", h.CreateUser)
e.GET("/", h.GetUsers)
e.GET("/:id", h.GetUsers)
e.PATCH("/:id", h.UpdateUser)
e.DELETE("/:id", h.DeleteUser)
e.GET("/health", h.Health)
e.Logger.Fatal(e.Start(":8081"))
}

20
model/user.go Normal file
View File

@@ -0,0 +1,20 @@
package model
import "gorm.io/gorm"
type (
User struct {
gorm.Model
UserName string `json:"userName" gorm:"unique"`
Password string `json:"password,omitempty"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}
)
type APIUser struct {
UserName string `json:"userName" gorm:"unique"`
Password string `json:"password,omitempty"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}

27
pkg/utils/query.go Normal file
View File

@@ -0,0 +1,27 @@
package utils
import (
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func ApplySort(tx *gorm.DB, column string, isDesc bool) {
tx.Order(clause.OrderByColumn{
Column: clause.Column{Name: column},
Desc: isDesc,
})
}
func ApplyFilters(tx *gorm.DB, column string, value string) {
if column != "" && value != "" {
tx.Where(column + " LIKE '%" + value + "%'")
}
}
func ApplyIdFilter(c echo.Context, tx *gorm.DB) {
id := c.Param("id")
if id != "" {
tx.Where("id = ?", id)
}
}

52
pkg/utils/url.go Normal file
View File

@@ -0,0 +1,52 @@
package utils
import (
"net/url"
"strconv"
"strings"
)
func GetQueryParam(query url.Values, key string, defaultVal string) string {
data := query.Get(key)
if data == "" {
return defaultVal
}
return data
}
func GetPaginationParams(query url.Values) (int, int) {
offset := GetQueryParam(query, "offset", "0")
limit := GetQueryParam(query, "limit", "10")
limitInt, err := strconv.Atoi(limit)
if err != nil {
limitInt = 10
}
offsetInt, err := strconv.Atoi(offset)
if err != nil {
offsetInt = 0
}
return limitInt, offsetInt
}
func GetFilterParams(query url.Values) (string, bool, string, string) {
sort := GetQueryParam(query, "sort", "id:asc")
filter := GetQueryParam(query, "filter", ":")
filters := strings.Split(filter, ":")
sorts := strings.Split(sort, ":")
isDesc := false
if sorts[1] == "desc" {
isDesc = true
}
return sorts[0], isDesc, filters[0], filters[1]
}

BIN
test.db Normal file

Binary file not shown.