Compare commits
11 Commits
1d6f517ad3
...
0fb494f0fd
Author | SHA1 | Date | |
---|---|---|---|
0fb494f0fd | |||
30ee6e8c39 | |||
bf3f5a0fc0 | |||
41292b25e3 | |||
ceb3c4bba5 | |||
69b4179102 | |||
4dab09e66b | |||
eb32faf8b2 | |||
10ecb6fd0d | |||
abfd08cc26 | |||
3d3ce8873c |
72
.drone.yml
Normal file
72
.drone.yml
Normal file
@ -0,0 +1,72 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: go_static_check
|
||||
image: golang:latest
|
||||
commands:
|
||||
- go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- staticcheck ./internal/...
|
||||
volumes:
|
||||
- name: gopath
|
||||
path: /go
|
||||
|
||||
- name: go_lint
|
||||
image: golang:latest
|
||||
commands:
|
||||
- go install golang.org/x/lint/golint@latest
|
||||
- golint ./internal/...
|
||||
volumes:
|
||||
- name: gopath
|
||||
path: /go
|
||||
|
||||
- name: go_vet
|
||||
image: golang:latest
|
||||
commands:
|
||||
- go vet ./internal/...
|
||||
volumes:
|
||||
- name: gopath
|
||||
path: /go
|
||||
|
||||
- name: build_image
|
||||
image: plugins/docker
|
||||
commands:
|
||||
- sleep 5
|
||||
- ./deploy/docker/build_image.sh
|
||||
depends_on:
|
||||
- go_static_check
|
||||
- go_lint
|
||||
- go_vet
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run
|
||||
|
||||
- name: push_image
|
||||
image: plugins/docker
|
||||
environment:
|
||||
DOCKER_USERNAME:
|
||||
from_secret: registry_username
|
||||
DOCKER_PASSWORD:
|
||||
from_secret: registry_password
|
||||
commands:
|
||||
- ./deploy/docker/publish_image.sh
|
||||
depends_on:
|
||||
- build_image
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run
|
||||
|
||||
services:
|
||||
- name: docker
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run
|
||||
|
||||
volumes:
|
||||
- name: gopath
|
||||
temp: {}
|
||||
- name: docker-sock
|
||||
temp: {}
|
10
.env.dist
10
.env.dist
@ -1,2 +1,10 @@
|
||||
HTTP_PORT=8000
|
||||
# Server config
|
||||
SERVER_IP=127.0.0.1
|
||||
SERVER_PORT=8000
|
||||
|
||||
# Auth config
|
||||
AUTH_SECRET_HMAC=HmacSecretToken
|
||||
AUTH_TOKEN_EXPIRE_TIME=5
|
||||
|
||||
# Database config
|
||||
DATABASE_URL=postgres://username:password@host:port/db_name
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
!/**/.gitkeep
|
||||
|
||||
/.idea
|
||||
.env
|
||||
/vendor
|
||||
|
||||
/.env
|
||||
/build/*
|
||||
/test/data/*
|
||||
/vendor
|
||||
|
9
Makefile
9
Makefile
@ -1,3 +1,5 @@
|
||||
.PHONY: run race build test bench-cpu bench-mem bench-trace bench-clean
|
||||
|
||||
run:
|
||||
go run cmd/server/main.go
|
||||
|
||||
@ -5,9 +7,9 @@ race:
|
||||
go run --race cmd/server/main.go
|
||||
|
||||
build:
|
||||
go build cmd/server/main.go
|
||||
GOOS=linux GOARCH=amd64 go build -o build/server cmd/server/main.go
|
||||
|
||||
tests:
|
||||
test:
|
||||
go test -v -run=. test/**/*.go
|
||||
|
||||
bench: bench-clean
|
||||
@ -25,6 +27,9 @@ bench-trace:
|
||||
bench-clean:
|
||||
rm -f test/data/*.out test/data/bench.log
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
# run-profiler:
|
||||
# go run cmd/server/main.go -cpuprofile cpu.prof -memprofile mem.prof
|
||||
#
|
||||
|
@ -9,31 +9,31 @@ import (
|
||||
"time"
|
||||
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/database"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/config"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/handler"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
const ( // FIXME
|
||||
defaultHTTPPort = "8080"
|
||||
defaultDatabaseUrl = "postgres://postgres:12345678@127.0.0.1:5434/S7"
|
||||
const (
|
||||
defHttpIp = "127.0.0.1"
|
||||
defHttpPort = "8080"
|
||||
defDbUrl = "postgres://postgres:postgres@127.0.0.1:5432/Api" // FIXME use default container conf in future
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load(".env")
|
||||
if err != nil {
|
||||
if config.ErrLoadingEnvs != nil {
|
||||
log.Fatalln("Error loading .env file")
|
||||
}
|
||||
|
||||
httpAddr := net.JoinHostPort("127.0.0.1", getEnv("HTTP_PORT", defaultHTTPPort))
|
||||
dbConnStr := getEnv("DATABASE_URL", defaultDatabaseUrl)
|
||||
httpAddr := net.JoinHostPort(config.GetEnv("SERVER_IP", defHttpIp), config.GetEnv("SERVER_PORT", defHttpPort))
|
||||
// dbConnStr := config.GetEnv("DATABASE_URL", defDbUrl)
|
||||
|
||||
dbc, err := database.Connect(dbConnStr)
|
||||
if err != nil {
|
||||
log.Panicf("Unable to connect to database: %v\n", err)
|
||||
}
|
||||
// dbc, err := database.Connect(dbConnStr)
|
||||
// if err != nil {
|
||||
// log.Panicf("Unable to connect to database: %v\n", err)
|
||||
// }
|
||||
|
||||
env := &handler.Env{httpAddr, dbc}
|
||||
// env := &handler.Env{httpAddr, dbc}
|
||||
env := &handler.Env{httpAddr, nil}
|
||||
srv := app.NewServer(env)
|
||||
|
||||
go srv.Start()
|
||||
@ -48,12 +48,3 @@ func main() {
|
||||
srv.Shutdown(ctx)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func getEnv(name, defVal string) string {
|
||||
env := os.Getenv(name)
|
||||
if env == "" {
|
||||
return defVal
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
26
deploy/docker/Dockerfile.build
Normal file
26
deploy/docker/Dockerfile.build
Normal file
@ -0,0 +1,26 @@
|
||||
# Builder
|
||||
FROM golang:alpine AS builder
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
ARG MAIN_GO=cmd/server/main.go
|
||||
|
||||
ARG SERVER_PORT=8080
|
||||
ENV SERVER_PORT {$SERVER_PORT}
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY internal ./internal
|
||||
COPY cmd ./cmd
|
||||
|
||||
RUN ls -lh
|
||||
|
||||
RUN go mod download && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app $MAIN_GO
|
||||
|
||||
# Destination image
|
||||
FROM gcr.io/distroless/base-debian10
|
||||
|
||||
COPY --from=builder /go/bin/app /app
|
||||
|
||||
EXPOSE $SERVER_PORT
|
||||
ENTRYPOINT ["/app"]
|
11
deploy/docker/Dockerfile.run
Normal file
11
deploy/docker/Dockerfile.run
Normal file
@ -0,0 +1,11 @@
|
||||
FROM golang:alpine
|
||||
|
||||
# RUN mkdir /go/src/app
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY pkg ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
ENTRYPOINT ["go", "run", "pkg/main.go"]
|
11
deploy/docker/build_image.sh
Executable file
11
deploy/docker/build_image.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -evx
|
||||
|
||||
branch=${DRONE_TAG:=$CI_COMMIT_BRANCH}
|
||||
branch=$(echo $branch | grep -v /) || echo $branch ;
|
||||
p1=$(echo $branch | cut -d / -f1 -s) &&
|
||||
p2=$(echo $branch | cut -d / -f2 -s) &&
|
||||
tag=${branch:=$p1-$p2} &&
|
||||
echo "Building" $tag
|
||||
|
||||
docker build -t git.pbiernat.dev/golang/rest-api-prototype:$tag -f deploy/docker/Dockerfile.build .
|
12
deploy/docker/publish_image.sh
Executable file
12
deploy/docker/publish_image.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -evx
|
||||
|
||||
branch=${DRONE_TAG:=$CI_COMMIT_BRANCH}
|
||||
branch=$(echo $branch | grep -v /) || echo $branch ;
|
||||
p1=$(echo $branch | cut -d / -f1 -s) &&
|
||||
p2=$(echo $branch | cut -d / -f2 -s) &&
|
||||
tag=${branch:=$p1-$p2} &&
|
||||
echo "Publishing" $tag
|
||||
|
||||
echo $DOCKER_PASSWORD | docker login git.pbiernat.dev -u $DOCKER_USERNAME --password-stdin &&
|
||||
docker push git.pbiernat.dev/golang/rest-api-prototype:$tag
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.17
|
||||
|
||||
require (
|
||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/jackc/pgx/v4 v4.15.0
|
||||
github.com/joho/godotenv v1.4.0
|
||||
|
2
go.sum
2
go.sum
@ -17,6 +17,8 @@ github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6m
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
|
21
init/api-server.service
Normal file
21
init/api-server.service
Normal file
@ -0,0 +1,21 @@
|
||||
#FIXME: FIX PATHS
|
||||
|
||||
[Unit]
|
||||
Requires=network-online.target
|
||||
After=network-online.target
|
||||
Requires=api-server.socket
|
||||
|
||||
[Service]
|
||||
ExecStart=/home/keedosn/go/src/git.pbiernat.dev/golang/rest-api-prototype/build/server
|
||||
ExecStop=/bin/kill $MAINPID
|
||||
WorkingDirectory=/home/keedosn/go/src/git.pbiernat.dev/golang/rest-api-prototype
|
||||
User=keedosn
|
||||
Group=keedosn
|
||||
NonBlocking=true
|
||||
StandardOutput=append:/home/keedosn/go/src/git.pbiernat.dev/golang/rest-api-prototype/build/api-server.log
|
||||
StandardError=append:/home/keedosn/go/src/git.pbiernat.dev/golang/rest-api-prototype/build/api-server.log
|
||||
SyslogIdentifier=api-server
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
7
init/api-server.socket
Normal file
7
init/api-server.socket
Normal file
@ -0,0 +1,7 @@
|
||||
[Unit]
|
||||
Description=API Server socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=10080
|
||||
NoDelay=true
|
||||
|
1
insomnia.json
Normal file
1
insomnia.json
Normal file
File diff suppressed because one or more lines are too long
22
internal/app/config/env.go
Normal file
22
internal/app/config/env.go
Normal file
@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var ErrLoadingEnvs error
|
||||
|
||||
func init() {
|
||||
ErrLoadingEnvs = godotenv.Load()
|
||||
}
|
||||
|
||||
func GetEnv(name, defVal string) string {
|
||||
env := os.Getenv(name)
|
||||
if env == "" {
|
||||
return defVal
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
16
internal/app/definition/article.go
Normal file
16
internal/app/definition/article.go
Normal file
@ -0,0 +1,16 @@
|
||||
package definition
|
||||
|
||||
import "git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
|
||||
|
||||
type CreateArticleRequest struct {
|
||||
CategoryID int `json:"category_id"`
|
||||
Title string `json:"title"`
|
||||
Intro string `json:"intro"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type CreateArticleResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data *entity.Article `json:"data"`
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
9
internal/app/definition/auth.go
Normal file
9
internal/app/definition/auth.go
Normal file
@ -0,0 +1,9 @@
|
||||
package definition
|
||||
|
||||
type AuthLoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AuthLoginResponse struct {
|
||||
}
|
27
internal/app/definition/category.go
Normal file
27
internal/app/definition/category.go
Normal file
@ -0,0 +1,27 @@
|
||||
package definition
|
||||
|
||||
import (
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
|
||||
validation "github.com/go-ozzo/ozzo-validation"
|
||||
)
|
||||
|
||||
type CreateCategoryRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (c CreateCategoryRequest) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Name, validation.Required, validation.Length(3, 255)),
|
||||
)
|
||||
}
|
||||
|
||||
type CreateCategoryResponse struct {
|
||||
Data *entity.Category `json:"data"`
|
||||
Err string `json:"err,omitempty"` // FIXME: omitempty on/off?
|
||||
}
|
||||
|
||||
type DeleteCategoryRequest struct {
|
||||
}
|
||||
|
||||
type DeleteCategoryResponse struct {
|
||||
}
|
9
internal/app/definition/error.go
Normal file
9
internal/app/definition/error.go
Normal file
@ -0,0 +1,9 @@
|
||||
package definition
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func Error(err string) *ErrorResponse {
|
||||
return &ErrorResponse{err}
|
||||
}
|
@ -8,7 +8,7 @@ type Category struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreateDate time.Time `json:"create_date"`
|
||||
ModifyDate time.Time `json:"modify_date"` // FIXME
|
||||
ModifyDate time.Time `json:"modify_date"` // FIXME: zero-value issue
|
||||
}
|
||||
|
||||
// func (c Category) Validate() error {
|
||||
|
18
internal/app/entity/user.go
Normal file
18
internal/app/entity/user.go
Normal file
@ -0,0 +1,18 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
CreateDate time.Time `json:"create_date"`
|
||||
ModifyDate time.Time `json:"modify_date"` // FIXME: zero-value issue
|
||||
}
|
||||
|
||||
var TestUser = &User{
|
||||
ID: 1,
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
CreateDate: time.Now(),
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
|
||||
)
|
||||
|
||||
@ -13,26 +14,13 @@ var CreateArticleHandler *Handler
|
||||
func init() {
|
||||
CreateArticleHandler = &Handler{
|
||||
Handle: CreateArticleHandlerFunc,
|
||||
Request: &CreateArticleRequest{},
|
||||
Response: &CreateArticleResponse{},
|
||||
Request: &def.CreateArticleRequest{},
|
||||
Response: &def.CreateArticleResponse{},
|
||||
}
|
||||
}
|
||||
|
||||
type CreateArticleRequest struct {
|
||||
CategoryID int `json:"category_id"`
|
||||
Title string `json:"title"`
|
||||
Intro string `json:"intro"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type CreateArticleResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data *entity.Article `json:"data"`
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
func CreateArticleHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
|
||||
var art = h.Request.(*CreateArticleRequest)
|
||||
var art = h.Request.(*def.CreateArticleRequest)
|
||||
log.Println(art)
|
||||
|
||||
return &entity.Article{
|
||||
@ -43,16 +31,4 @@ func CreateArticleHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, i
|
||||
Text: "Text",
|
||||
CreateDate: time.Now(),
|
||||
}, http.StatusCreated, nil
|
||||
|
||||
// return &CreateArticleResponse{
|
||||
// Status: http.StatusText(http.StatusOK),
|
||||
// Data: &entity.Article{
|
||||
// ID: 1,
|
||||
// CategoryID: 1,
|
||||
// Title: "Dummy article",
|
||||
// Intro: "Intro",
|
||||
// Text: "Text",
|
||||
// CreateDate: time.Now(),
|
||||
// },
|
||||
// }, http.StatusCreated, nil
|
||||
}
|
||||
|
35
internal/app/handler/auth.go
Normal file
35
internal/app/handler/auth.go
Normal file
@ -0,0 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/service"
|
||||
)
|
||||
|
||||
var AuthLoginHandler *Handler
|
||||
|
||||
func init() {
|
||||
AuthLoginHandler = &Handler{
|
||||
Handle: AuthLoginHandlerFunc,
|
||||
Request: &def.AuthLoginRequest{},
|
||||
Response: &def.AuthLoginResponse{},
|
||||
}
|
||||
}
|
||||
|
||||
func AuthLoginHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
|
||||
var req = h.Request.(*def.AuthLoginRequest)
|
||||
// u := entity.TestUser
|
||||
|
||||
token, err := service.AuthService.Login(req)
|
||||
if err != nil {
|
||||
return nil, http.StatusUnauthorized, err
|
||||
}
|
||||
|
||||
service.AuthService.SetCookie(w, service.AuthService.TokenCookieName, token)
|
||||
// service.AuthService.SetCookie(w, service.AuthService.RefreshTokenCookieName, refreshTtoken)
|
||||
|
||||
// log.Println("user:", u, "req:", token, "err:", err)
|
||||
|
||||
return nil, http.StatusOK, nil
|
||||
}
|
@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
|
||||
validation "github.com/go-ozzo/ozzo-validation"
|
||||
)
|
||||
|
||||
var CreateCategoryHandler *Handler
|
||||
@ -16,40 +16,19 @@ var DeleteCategoryHandler *Handler
|
||||
func init() {
|
||||
CreateCategoryHandler = &Handler{
|
||||
Handle: CreateCategoryHandlerFunc,
|
||||
Request: &CreateCategoryRequest{},
|
||||
Response: &CreateCategoryResponse{},
|
||||
Request: &def.CreateCategoryRequest{},
|
||||
Response: &def.CreateCategoryResponse{},
|
||||
}
|
||||
|
||||
DeleteCategoryHandler = &Handler{
|
||||
Handle: DeleteCategoryHandlerFunc,
|
||||
Request: &DeleteCategoryRequest{},
|
||||
Response: &DeleteCategoryResponse{},
|
||||
Request: &def.DeleteCategoryRequest{},
|
||||
Response: &def.DeleteCategoryResponse{},
|
||||
}
|
||||
}
|
||||
|
||||
type CreateCategoryRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (c CreateCategoryRequest) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Name, validation.Required, validation.Length(3, 255)),
|
||||
)
|
||||
}
|
||||
|
||||
type CreateCategoryResponse struct {
|
||||
Data *entity.Category `json:"data"`
|
||||
Err string `json:"err,omitempty"` // FIXME: omitempty on/off?
|
||||
}
|
||||
|
||||
type DeleteCategoryRequest struct {
|
||||
}
|
||||
|
||||
type DeleteCategoryResponse struct {
|
||||
}
|
||||
|
||||
func CreateCategoryHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
|
||||
var cat = h.Request.(*CreateCategoryRequest)
|
||||
var cat = h.Request.(*def.CreateCategoryRequest)
|
||||
log.Println("Cat input:", cat)
|
||||
|
||||
if err := cat.Validate(); err != nil {
|
||||
@ -61,25 +40,18 @@ func CreateCategoryHandlerFunc(h *Handler, w http.ResponseWriter) (interface{},
|
||||
Name: cat.Name,
|
||||
CreateDate: time.Now(),
|
||||
}, http.StatusCreated, nil
|
||||
|
||||
// return &CreateCategoryResponse{
|
||||
// Data: &entity.Category{
|
||||
// Name: "Dummy category",
|
||||
// CreateDate: time.Now(),
|
||||
// },
|
||||
// }, http.StatusCreated, nil
|
||||
}
|
||||
|
||||
func DeleteCategoryHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
|
||||
var cat = h.Request.(*DeleteCategoryRequest)
|
||||
var cat = h.Request.(*def.DeleteCategoryRequest)
|
||||
log.Println(cat)
|
||||
|
||||
id, _ := strconv.Atoi(h.Params["id"])
|
||||
log.Println(h.Params)
|
||||
|
||||
if id != 1 {
|
||||
return &DeleteCategoryResponse{}, http.StatusNotFound, nil
|
||||
return nil, http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
return &DeleteCategoryResponse{}, http.StatusNoContent, nil
|
||||
return nil, http.StatusNoContent, nil
|
||||
}
|
||||
|
@ -2,14 +2,6 @@ package handler
|
||||
|
||||
import "net/http"
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func Error(err string) *ErrorResponse {
|
||||
return &ErrorResponse{Error: err}
|
||||
}
|
||||
|
||||
type NotFoundHandler struct{}
|
||||
|
||||
func (NotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
)
|
||||
@ -33,13 +34,16 @@ type response struct {
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func New(e *Env, h *Handler) *Handler {
|
||||
return &Handler{e, h.Handle, h.Request, h.Response, Set{}}
|
||||
func Init(e *Env, h *Handler) *Handler {
|
||||
// return &Handler{e, h.Handle, h.Request, h.Response, Set{}}
|
||||
h.Env = e
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := decodeRequestData(r, h.Request); err != nil {
|
||||
log.Println("err_ServeHTTP:", err.Error())
|
||||
log.Println("Decode request data error:", err.Error())
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
@ -67,11 +71,13 @@ func encodeResponse(w http.ResponseWriter, res *response, err error) {
|
||||
}
|
||||
|
||||
w.WriteHeader(res.Status)
|
||||
json.NewEncoder(w).Encode(res.Data)
|
||||
if res.Data != nil {
|
||||
json.NewEncoder(w).Encode(res.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeError(w http.ResponseWriter, status int, e error) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
json.NewEncoder(w).Encode(Error(e.Error()))
|
||||
json.NewEncoder(w).Encode(def.Error(e.Error()))
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/handler"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/service"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@ -17,13 +18,18 @@ func SetupRouter(env *handler.Env) *mux.Router {
|
||||
r.Use(LoggingMiddleware)
|
||||
|
||||
hc := r.PathPrefix("/health").Subrouter()
|
||||
hc.Handle("", handler.New(env, handler.HealthCheckHandler)).Methods(http.MethodGet)
|
||||
hc.Handle("", handler.Init(env, handler.HealthCheckHandler)).Methods(http.MethodGet)
|
||||
|
||||
auth := r.PathPrefix("/auth").Subrouter()
|
||||
auth.Handle("/login", handler.Init(env, handler.AuthLoginHandler)).Methods(http.MethodPost)
|
||||
|
||||
api := r.PathPrefix("/api").Subrouter()
|
||||
api.Handle("/article", handler.New(env, handler.CreateArticleHandler)).Methods(http.MethodPost)
|
||||
api.Use(service.AuthService.ValidateUserTokenMiddleware) // only /api/** endpoints use this middleware
|
||||
|
||||
api.Handle("/category", handler.New(env, handler.CreateCategoryHandler)).Methods(http.MethodPost)
|
||||
api.Handle("/category/{id:[0-9]+}", handler.New(env, handler.DeleteCategoryHandler)).Methods(http.MethodDelete)
|
||||
api.Handle("/article", handler.Init(env, handler.CreateArticleHandler)).Methods(http.MethodPost)
|
||||
|
||||
api.Handle("/category", handler.Init(env, handler.CreateCategoryHandler)).Methods(http.MethodPost)
|
||||
api.Handle("/category/{id:[0-9]+}", handler.Init(env, handler.DeleteCategoryHandler)).Methods(http.MethodDelete)
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -5,9 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/handler"
|
||||
)
|
||||
|
||||
@ -28,20 +32,23 @@ func NewServer(env *handler.Env) *Server {
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
log.Println("Server listening on " + s.Addr)
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.Println(err)
|
||||
if os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getpid()) {
|
||||
// systemd run
|
||||
f := os.NewFile(3, "from systemd")
|
||||
l, err := net.FileListener(f)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
log.Println("Server listening on " + l.Addr().String())
|
||||
s.Serve(l)
|
||||
} else {
|
||||
|
||||
log.Println("Server listening on " + s.Addr)
|
||||
log.Fatalln(s.ListenAndServe())
|
||||
}
|
||||
}
|
||||
|
||||
// func (s *Server) Shutdown(ctx context.Context) {
|
||||
// log.Println("Shutting down...")
|
||||
|
||||
// if err := s.Shutdown(ctx); err != nil {
|
||||
// log.Panicln(err)
|
||||
// }
|
||||
// }
|
||||
|
||||
func PrepareHeadersMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
@ -57,22 +64,9 @@ func ValidateJsonBodyMiddleware(next http.Handler) http.Handler {
|
||||
buf, _ := ioutil.ReadAll(r.Body)
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(buf)) // rollack *Request to original state
|
||||
|
||||
// if len(buf) > 0 {
|
||||
// next.ServeHTTP(w, r)
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
// if !json.Valid(buf) {
|
||||
// w.WriteHeader(http.StatusBadRequest)
|
||||
// json.NewEncoder(w).Encode(handler.Error("Unable to parse JSON: " + string(buf)))
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
if len(buf) > 0 && !json.Valid(buf) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(handler.Error("Unable to parse JSON: " + string(buf)))
|
||||
json.NewEncoder(w).Encode(def.Error("Unable to parse JSON: " + string(buf)))
|
||||
|
||||
return
|
||||
}
|
||||
|
113
internal/app/service/auth.go
Normal file
113
internal/app/service/auth.go
Normal file
@ -0,0 +1,113 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/config"
|
||||
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
AuthService *Auth
|
||||
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrTokenError = errors.New("failed to generate JWT token")
|
||||
)
|
||||
|
||||
func init() {
|
||||
expire, _ := strconv.Atoi(config.GetEnv("AUTH_TOKEN_EXPIRE_TIME", "5"))
|
||||
secret := []byte(config.GetEnv("AUTH_SECRET_HMAC", "B413IlIv9nKQfsMCXTE0Cteo4yHgUEfqaLfjg73sNlh"))
|
||||
|
||||
AuthService = &Auth{expire, "jwt_token", "jwt_token_refresh", secret}
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
ExpireTime int // token expire time in minutes
|
||||
TokenCookieName string
|
||||
RefreshTokenCookieName string
|
||||
|
||||
secret []byte // signing key
|
||||
}
|
||||
|
||||
func (a *Auth) Login(r *def.AuthLoginRequest) (string, error) {
|
||||
if r.Username == "admin" && r.Password == "secret" {
|
||||
token, err := a.createToken()
|
||||
if err != nil {
|
||||
return "", ErrTokenError
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return "", ErrUserNotFound
|
||||
}
|
||||
|
||||
// SetCookie appends cookie header to response
|
||||
func (a *Auth) SetCookie(w http.ResponseWriter, name, token string) {
|
||||
c := &http.Cookie{
|
||||
Name: name,
|
||||
Value: token,
|
||||
MaxAge: a.ExpireTime * 60,
|
||||
Path: "/",
|
||||
}
|
||||
http.SetCookie(w, c)
|
||||
}
|
||||
|
||||
func (a Auth) createToken() (string, error) {
|
||||
// log.Println("now:", time.Now().Unix())
|
||||
// log.Println("expire at:", time.Now().Add(time.Duration(a.ExpireTime)*time.Minute).Unix())
|
||||
claims := &jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(time.Duration(a.ExpireTime) * time.Minute).Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(a.secret)
|
||||
}
|
||||
|
||||
func (a *Auth) validateToken(tokenStr string) error {
|
||||
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
|
||||
// Don't forget to validate the alg is what you expect:
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
|
||||
return a.secret, nil
|
||||
})
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
log.Println(claims)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a Auth) ValidateUserTokenMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cToken, err := r.Cookie(a.TokenCookieName)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(def.Error("Missing JWT Token cookie"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.validateToken(cToken.Value); err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(def.Error(err.Error()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
@ -18,9 +18,5 @@ func TestCreateArticleHandler(t *testing.T) {
|
||||
if responseRecorder.Code != http.StatusCreated {
|
||||
t.Errorf("Want status '%d', got '%d'", http.StatusCreated, responseRecorder.Code)
|
||||
}
|
||||
|
||||
// if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
|
||||
// t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/handler"
|
||||
@ -10,7 +11,8 @@ import (
|
||||
|
||||
func TestCreateCategoryHandler(t *testing.T) {
|
||||
t.Run("test create category handler", func(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/category", nil)
|
||||
body := "{\"name\":\"Test Category\"}"
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/category", strings.NewReader(body))
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
|
||||
handler.CreateCategoryHandler.ServeHTTP(responseRecorder, request)
|
||||
@ -18,9 +20,5 @@ func TestCreateCategoryHandler(t *testing.T) {
|
||||
if responseRecorder.Code != http.StatusCreated {
|
||||
t.Errorf("Want status '%d', got '%d'", http.StatusCreated, responseRecorder.Code)
|
||||
}
|
||||
|
||||
// if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
|
||||
// t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user