init
This commit is contained in:
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
/bin/
|
||||||
|
/web/*
|
||||||
|
/config/config.yaml
|
||||||
20
Makefile
Normal file
20
Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
BINARY_NAME=server
|
||||||
|
MAIN_PACKAGE_PATH = ./cmd/main.go
|
||||||
|
|
||||||
|
.PHONY: tidy
|
||||||
|
tidy:
|
||||||
|
go mod tidy -v
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
go build -o bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH}
|
||||||
|
# GOARCH=arm64 GOOS=darwin go build -o bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH}
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: build
|
||||||
|
./bin/${BINARY_NAME}
|
||||||
|
|
||||||
|
.PHONY: gen
|
||||||
|
gen:
|
||||||
|
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/hello.proto
|
||||||
43
cmd/grpc.go
Normal file
43
cmd/grpc.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/reflection"
|
||||||
|
"log"
|
||||||
|
pb "madsky.ru/go-tracker/proto"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var port = flag.Int("port", 50051, "the server port")
|
||||||
|
|
||||||
|
type GrpcServer struct {
|
||||||
|
pb.UnimplementedHelloServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GrpcServer) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
|
||||||
|
log.Printf("Received: %v", in.GetName())
|
||||||
|
return &pb.HelloResponse{Message: "Hello " + in.GetName()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func grpcServer() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
listen, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := grpc.NewServer()
|
||||||
|
|
||||||
|
reflection.Register(s)
|
||||||
|
pb.RegisterHelloServer(s, &GrpcServer{})
|
||||||
|
|
||||||
|
log.Printf("server listening at %v", listen.Addr())
|
||||||
|
|
||||||
|
if err := s.Serve(listen); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
cmd/main.go
Normal file
69
cmd/main.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"madsky.ru/go-tracker/internal/broker"
|
||||||
|
"madsky.ru/go-tracker/internal/config"
|
||||||
|
"madsky.ru/go-tracker/internal/database"
|
||||||
|
"madsky.ru/go-tracker/internal/logger"
|
||||||
|
"madsky.ru/go-tracker/internal/server"
|
||||||
|
"madsky.ru/go-tracker/internal/storage"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.MustLoad("./config/config.yaml")
|
||||||
|
|
||||||
|
log := logger.NewLogger()
|
||||||
|
dsn := getDSN(cfg.Database.Host, cfg.Database.Port, cfg.Database.User, cfg.Database.Password, cfg.Database.Name)
|
||||||
|
|
||||||
|
//ctx := context.WithValue(context.Background(), "value", "foo bar")
|
||||||
|
//ctx := context.Background()
|
||||||
|
ctx, cancel := context.WithCancel(context.WithValue(context.Background(), "value", "foo bar"))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// broker section
|
||||||
|
n := broker.NewBroker(cfg)
|
||||||
|
defer n.Close()
|
||||||
|
|
||||||
|
pubErr := n.Publish("test", []byte("foobar"))
|
||||||
|
if pubErr != nil {
|
||||||
|
fmt.Println(pubErr)
|
||||||
|
}
|
||||||
|
_, subErr := n.Subscribe("test", func(msg *nats.Msg) {
|
||||||
|
fmt.Println(msg.Subject, string(msg.Data))
|
||||||
|
})
|
||||||
|
if subErr != nil {
|
||||||
|
log.Error("Error subscribing", subErr)
|
||||||
|
}
|
||||||
|
// end broker section
|
||||||
|
|
||||||
|
client, err := database.NewClient(ctx, dsn, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error connecting to database", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
store := storage.NewStorage(client)
|
||||||
|
|
||||||
|
app := &server.Application{
|
||||||
|
Config: cfg,
|
||||||
|
Storage: store,
|
||||||
|
Ctx: ctx,
|
||||||
|
}
|
||||||
|
r := app.Routes()
|
||||||
|
|
||||||
|
if err = app.Start(r); err != nil {
|
||||||
|
log.Error("Error starting http server", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDSN(host, port, user, password, name string) string {
|
||||||
|
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, port, user, password, name)
|
||||||
|
}
|
||||||
10
config/example.config.yaml
Normal file
10
config/example.config.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
app_name: 'APP'
|
||||||
|
nats:
|
||||||
|
host: ''
|
||||||
|
token: ''
|
||||||
|
database:
|
||||||
|
host: ''
|
||||||
|
port: 5432
|
||||||
|
user: ''
|
||||||
|
password: ''
|
||||||
|
name: 'tracker'
|
||||||
41
go.mod
Normal file
41
go.mod
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
module madsky.ru/go-tracker
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
|
github.com/goccy/go-json v0.10.5
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
|
github.com/nats-io/nats.go v1.47.0
|
||||||
|
golang.org/x/crypto v0.44.0
|
||||||
|
google.golang.org/grpc v1.76.0
|
||||||
|
google.golang.org/protobuf v1.36.10
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.1 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
golang.org/x/net v0.47.0 // indirect
|
||||||
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||||
|
)
|
||||||
124
go.sum
Normal file
124
go.sum
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
|
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||||
|
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||||
23
internal/broker/broker.go
Normal file
23
internal/broker/broker.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"madsky.ru/go-tracker/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewBroker(cfg *config.Config) *nats.Conn {
|
||||||
|
if cfg.Nats.Token == "" {
|
||||||
|
panic("NATS Token not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("nats://%s@%s:%s", cfg.Nats.Token, cfg.Nats.Host, cfg.Nats.Port)
|
||||||
|
|
||||||
|
nc, err := nats.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nc
|
||||||
|
}
|
||||||
42
internal/config/config.go
Normal file
42
internal/config/config.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ilyakaznacheev/cleanenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AppName string `yaml:"app_name" env:"APP_NAME" env-default:"test"`
|
||||||
|
Server Server `yaml:"server"`
|
||||||
|
Database Database `yaml:"database"`
|
||||||
|
Nats Nats `yaml:"nats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Port string `yaml:"port" env:"PORT" env-default:"3000"`
|
||||||
|
Host string `yaml:"host" env:"HOST" env-default:"localhost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Nats struct {
|
||||||
|
Host string `yaml:"host" env:"NATS_HOST" env-default:"localhost"`
|
||||||
|
Port string `yaml:"port" env:"NATS_PORT" env-default:"4222"`
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port string `yaml:"port"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustLoad(path string) *Config {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
err := cleanenv.ReadConfig(path, &config)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config
|
||||||
|
}
|
||||||
30
internal/database/client.go
Normal file
30
internal/database/client.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error)
|
||||||
|
Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
|
||||||
|
QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
|
||||||
|
Begin(ctx context.Context) (pgx.Tx, error)
|
||||||
|
Ping(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(ctx context.Context, connUrl string, logger *slog.Logger) (*pgxpool.Pool, error) {
|
||||||
|
pool, err := pgxpool.New(ctx, connUrl)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(fmt.Fprintf(os.Stderr, "Unable to create connection pool: %v\n", err))
|
||||||
|
logger.Error("Unable to create connection pool: ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
12
internal/helpers/helpers.go
Normal file
12
internal/helpers/helpers.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintData(label string, data interface{}) {
|
||||||
|
fmt.Printf("---%s %#v\n", label, data)
|
||||||
|
//fmt.Printf("*** CONFIG %+v\n", app.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// const connUrl = "postgres://postgres:postgres@localhost:5432/go-finance"
|
||||||
10
internal/logger/logger.go
Normal file
10
internal/logger/logger.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLogger() *slog.Logger {
|
||||||
|
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||||
|
}
|
||||||
12
internal/model/auth/auth.go
Normal file
12
internal/model/auth/auth.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
type LoginDTO struct {
|
||||||
|
Email string `json:"email" validate:"required"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterDTO struct {
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
}
|
||||||
32
internal/model/issue/issue.go
Normal file
32
internal/model/issue/issue.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"madsky.ru/go-tracker/internal/model/project"
|
||||||
|
"madsky.ru/go-tracker/internal/model/status"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Issue struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Position uint32 `json:"position"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
StatusID uint32 `json:"status_id"`
|
||||||
|
ProjectID uint32 `json:"project_id"`
|
||||||
|
Status status.Status `json:"status"`
|
||||||
|
Project project.Project `json:"project"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateIssueDTO struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StatusID uint32 `json:"status_id"`
|
||||||
|
ProjectID uint32 `json:"project_id"`
|
||||||
|
Position *uint32 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PositionDTO struct {
|
||||||
|
StatusId uint32 `json:"status_id"`
|
||||||
|
Positions []uint32 `json:"positions"`
|
||||||
|
}
|
||||||
23
internal/model/project/project.go
Normal file
23
internal/model/project/project.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProjectDTO struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProjectDTO struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterDTO struct {
|
||||||
|
UserID *uint32 `json:"user_id"`
|
||||||
|
}
|
||||||
7
internal/model/setting/setting.go
Normal file
7
internal/model/setting/setting.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
type Setting struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
13
internal/model/status/status.go
Normal file
13
internal/model/status/status.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Position uint32 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateStatusDTO struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
27
internal/model/user/user.go
Normal file
27
internal/model/user/user.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserDTO struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserDTO struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseDTO struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
150
internal/server/auth.go
Normal file
150
internal/server/auth.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"madsky.ru/go-tracker/internal/model/auth"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
"madsky.ru/go-tracker/internal/server/helpers"
|
||||||
|
"madsky.ru/go-tracker/internal/server/response"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TTL = 12
|
||||||
|
|
||||||
|
var validate *validator.Validate
|
||||||
|
|
||||||
|
func (app *Application) login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var loginDto auth.LoginDTO
|
||||||
|
|
||||||
|
if err := dec.Decode(&loginDto); err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate = validator.New(validator.WithRequiredStructEnabled())
|
||||||
|
err := validate.Struct(loginDto)
|
||||||
|
if err != nil {
|
||||||
|
var validateErrs validator.ValidationErrors
|
||||||
|
|
||||||
|
errorsMessages := make([]string, 0)
|
||||||
|
if errors.As(err, &validateErrs) {
|
||||||
|
for _, e := range validateErrs {
|
||||||
|
errorsMessages = append(errorsMessages, fmt.Sprintf("%s is %s", e.Field(), e.Tag()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Error(w, errors.New(strings.Join(errorsMessages, ",")), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := app.Storage.User.FindByEmail(app.Ctx, loginDto.Email)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, errors.New("user does not exist"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !helpers.VerifyHashString(loginDto.Password, u.PasswordHash) {
|
||||||
|
response.Error(w, errors.New("wrong password"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expires := time.Now().Add(time.Hour * TTL)
|
||||||
|
token, err := helpers.CreateToken(u.ID, u.Role, expires)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]string)
|
||||||
|
data["token"] = token
|
||||||
|
data["exp"] = expires.UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "token",
|
||||||
|
Path: "/",
|
||||||
|
Value: token,
|
||||||
|
Expires: expires,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusCreated, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) register(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var registerDto auth.RegisterDTO
|
||||||
|
|
||||||
|
if err := dec.Decode(®isterDto); err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate = validator.New(validator.WithRequiredStructEnabled())
|
||||||
|
err := validate.Struct(registerDto)
|
||||||
|
if err != nil {
|
||||||
|
var validateErrs validator.ValidationErrors
|
||||||
|
|
||||||
|
errorsMessages := make([]string, 0)
|
||||||
|
if errors.As(err, &validateErrs) {
|
||||||
|
for _, e := range validateErrs {
|
||||||
|
errorsMessages = append(errorsMessages, fmt.Sprintf("%s is %s", e.Field(), e.Tag()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Error(w, errors.New(strings.Join(errorsMessages, ",")), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty, err := app.Storage.User.IsEmpty(app.Ctx)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newUser user.User
|
||||||
|
|
||||||
|
passwordHash, err := helpers.CreateHashString(registerDto.Password)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser.Email = registerDto.Email
|
||||||
|
newUser.Name = registerDto.Name
|
||||||
|
newUser.PasswordHash = passwordHash
|
||||||
|
if isEmpty {
|
||||||
|
newUser.Role = "admin"
|
||||||
|
} else {
|
||||||
|
newUser.Role = "user"
|
||||||
|
}
|
||||||
|
res, err := app.Storage.User.Create(app.Ctx, &newUser)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, response.ToUserResponseDto(res))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) logout(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "token",
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: -1,
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
response.WriteJSON(w, nil, http.StatusCreated, 1)
|
||||||
|
}
|
||||||
16
internal/server/health-check.go
Normal file
16
internal/server/health-check.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"madsky.ru/go-tracker/internal/server/response"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s, err := app.Storage.ServerSettings.Find(app.Ctx)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &s)
|
||||||
|
}
|
||||||
63
internal/server/helpers/helpers.go
Normal file
63
internal/server/helpers/helpers.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var secretKey = []byte("1234567890")
|
||||||
|
|
||||||
|
type TokenClaims struct {
|
||||||
|
UserId uint32 `json:"user_id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Expires int64 `json:"expires"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateToken(userId uint32, role string, expires time.Time) (string, error) {
|
||||||
|
claims := TokenClaims{
|
||||||
|
UserId: userId,
|
||||||
|
Role: role,
|
||||||
|
Expires: expires.Unix(),
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expires),
|
||||||
|
Issuer: "https://madsky.ru",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
ss, err := token.SignedString(secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyToken(tokenString string) (*TokenClaims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return secretKey, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("unknown claims type, cannot proceed")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateHashString(password string) (string, error) {
|
||||||
|
cost := bcrypt.DefaultCost
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyHashString(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
104
internal/server/issue.go
Normal file
104
internal/server/issue.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"madsky.ru/go-tracker/internal/model/issue"
|
||||||
|
"madsky.ru/go-tracker/internal/server/request"
|
||||||
|
"madsky.ru/go-tracker/internal/server/response"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) findIssues(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
i, err := app.Storage.Issues.Find(app.Ctx)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) findIssuesByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := request.Param(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := app.Storage.Issues.FindOne(app.Ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) createIssues(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var issueDto issue.CreateIssueDTO
|
||||||
|
|
||||||
|
err := dec.Decode(&issueDto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := app.Storage.Issues.Create(app.Ctx, &issueDto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := app.Storage.Issues.FindOne(app.Ctx, *id)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) updatePositions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var positionDto issue.PositionDTO
|
||||||
|
|
||||||
|
err := dec.Decode(&positionDto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i := make([]*uint32, 0)
|
||||||
|
|
||||||
|
for position, issueId := range positionDto.Positions {
|
||||||
|
id, err := app.Storage.Issues.UpdatePositions(app.Ctx, position, positionDto.StatusId, issueId)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i = append(i, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) deleteIssues(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := request.Param(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := app.Storage.Issues.Remove(app.Ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, count)
|
||||||
|
}
|
||||||
57
internal/server/middleware.go
Normal file
57
internal/server/middleware.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
"madsky.ru/go-tracker/internal/server/helpers"
|
||||||
|
"madsky.ru/go-tracker/internal/server/response"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserContext struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) AuthMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("token")
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, http.ErrNoCookie):
|
||||||
|
response.Error(w, errors.New("no authentication token"), http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
response.Error(w, errors.New("unknown error"), http.StatusForbidden)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := helpers.VerifyToken(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, errors.New("invalid authentication token"), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := app.Storage.User.FindById(app.Ctx, claims.UserId)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, errors.New("user not found"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), UserContext{}, u)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) LogRole(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
isAdmin := u.Role == "admin"
|
||||||
|
|
||||||
|
fmt.Println(fmt.Sprintf("is admin: %v", isAdmin))
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
48
internal/server/profile.go
Normal file
48
internal/server/profile.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
"madsky.ru/go-tracker/internal/server/response"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) getProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
res := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, response.ToUserResponseDto(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) updateProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var dto user.UpdateUserDTO
|
||||||
|
|
||||||
|
if err := dec.Decode(&dto); err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := app.Storage.User.Update(app.Ctx, u.ID, &dto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, response.ToUserResponseDto(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) deleteProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
fmt.Println(u)
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, nil)
|
||||||
|
}
|
||||||
147
internal/server/project.go
Normal file
147
internal/server/project.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"madsky.ru/go-tracker/internal/model/project"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
"madsky.ru/go-tracker/internal/server/request"
|
||||||
|
"madsky.ru/go-tracker/internal/server/response"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) findProjects(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
var filter project.FilterDTO
|
||||||
|
|
||||||
|
if u.Role != "admin" {
|
||||||
|
filter.UserID = &u.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin := u.Role == "admin"
|
||||||
|
|
||||||
|
p, err := app.Storage.Projects.Find(app.Ctx, u.ID, isAdmin, &filter)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) findProjectByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
projectId, err := request.Param(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter project.FilterDTO
|
||||||
|
|
||||||
|
if u.Role != "admin" {
|
||||||
|
filter.UserID = &u.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin := u.Role == "admin"
|
||||||
|
|
||||||
|
p, err := app.Storage.Projects.FindOne(app.Ctx, projectId, u.ID, isAdmin)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) createProject(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var projectDto project.CreateProjectDTO
|
||||||
|
|
||||||
|
if err := dec.Decode(&projectDto); err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate = validator.New(validator.WithRequiredStructEnabled())
|
||||||
|
err := validate.Struct(projectDto)
|
||||||
|
if err != nil {
|
||||||
|
var validateErrs validator.ValidationErrors
|
||||||
|
|
||||||
|
errorsMessages := make([]string, 0)
|
||||||
|
if errors.As(err, &validateErrs) {
|
||||||
|
for _, e := range validateErrs {
|
||||||
|
errorsMessages = append(errorsMessages, fmt.Sprintf("%s is %s", e.Field(), e.Tag()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Error(w, errors.New(strings.Join(errorsMessages, ",")), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := app.Storage.Projects.Create(app.Ctx, &projectDto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.Storage.UserToProject.Create(app.Ctx, u.ID, p.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusCreated, &p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) updateProject(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := request.Param(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var projectDto project.UpdateProjectDTO
|
||||||
|
|
||||||
|
if err = dec.Decode(&projectDto); err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := app.Storage.Projects.Update(app.Ctx, id, &projectDto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) deleteProject(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := request.Param(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := app.Storage.Projects.Remove(app.Ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, count)
|
||||||
|
}
|
||||||
21
internal/server/request/request.go
Normal file
21
internal/server/request/request.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Param(r *http.Request, name string) (uint64, error) {
|
||||||
|
idStr := r.PathValue(name)
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id < 1 {
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
60
internal/server/response/response.go
Normal file
60
internal/server/response/response.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusOK = "Success"
|
||||||
|
StatusError = "Error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteJSON(w http.ResponseWriter, message *string, code int, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
statusText := StatusOK
|
||||||
|
if code >= 400 {
|
||||||
|
statusText = StatusError
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(&Response{
|
||||||
|
Status: statusText,
|
||||||
|
Message: message,
|
||||||
|
Data: data,
|
||||||
|
}); err != nil {
|
||||||
|
http.Error(w, "unknown error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *CustomError) Error() string {
|
||||||
|
return fmt.Sprintf("custom error: %s", e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(w http.ResponseWriter, err error, code int) {
|
||||||
|
errorMessage := err.Error()
|
||||||
|
WriteJSON(w, &errorMessage, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToUserResponseDto(u *user.User) *user.ResponseDTO {
|
||||||
|
var ur user.ResponseDTO
|
||||||
|
ur.ID = u.ID
|
||||||
|
ur.Email = u.Email
|
||||||
|
ur.Role = u.Role
|
||||||
|
ur.Name = u.Name
|
||||||
|
|
||||||
|
return &ur
|
||||||
|
}
|
||||||
15
internal/server/server-settings.go
Normal file
15
internal/server/server-settings.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) getSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
|
||||||
|
fmt.Println(u)
|
||||||
|
return
|
||||||
|
}
|
||||||
157
internal/server/server.go
Normal file
157
internal/server/server.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"madsky.ru/go-tracker/internal/config"
|
||||||
|
"madsky.ru/go-tracker/internal/storage"
|
||||||
|
web "madsky.ru/go-tracker/web"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
Config *config.Config
|
||||||
|
Storage *storage.Storage
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(ctx context.Context, client *pgxpool.Pool, logger *slog.Logger) {
|
||||||
|
//statusRepository := status.NewRepository(client)
|
||||||
|
//projectsRepository := project.NewRepository(client)
|
||||||
|
//issueRepository := issue.NewRepository(client)
|
||||||
|
//userRepository := user.NewRepository(client)
|
||||||
|
|
||||||
|
//r.Get("/bla/assets/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// dist, err := fs.Sub(web.DistDir, "dist")
|
||||||
|
// if err != nil {
|
||||||
|
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
// }
|
||||||
|
// http.FileServer(http.FS(dist)).ServeHTTP(w, r)
|
||||||
|
//})
|
||||||
|
|
||||||
|
//handlers.RegisterAuthRoutes(mux, ctx, userRepository)
|
||||||
|
//
|
||||||
|
//authMux := http.NewServeMux()
|
||||||
|
//handlers.RegisterProjectRoutes(authMux, ctx, projectsRepository)
|
||||||
|
//handlers.RegisterStatusRoutes(authMux, ctx, statusRepository)
|
||||||
|
//handlers.RegisterIssueRoutes(authMux, ctx, issueRepository)
|
||||||
|
//handlers.RegisterUserRoutes(authMux, ctx, userRepository)
|
||||||
|
//
|
||||||
|
//mux.Handle("/api/", http.StripPrefix("/api", md.AuthMiddleware(authMux)))
|
||||||
|
//
|
||||||
|
//handler := md.LoggingMiddleware(mux, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) Routes() http.Handler {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
r.Use(middleware.RequestID)
|
||||||
|
r.Use(middleware.RealIP)
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
|
r.Route("/api", func(r chi.Router) {
|
||||||
|
r.Route("/version", func(r chi.Router) {
|
||||||
|
r.Get("/", app.getVersion)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/auth", func(r chi.Router) {
|
||||||
|
r.Post("/login", app.login)
|
||||||
|
r.Post("/register", app.register)
|
||||||
|
r.Post("/logout", app.logout)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/projects", func(r chi.Router) {
|
||||||
|
r.Use(app.AuthMiddleware)
|
||||||
|
r.Use(app.LogRole)
|
||||||
|
r.Get("/", app.findProjects)
|
||||||
|
r.Post("/", app.createProject)
|
||||||
|
|
||||||
|
r.Route("/{id}", func(r chi.Router) {
|
||||||
|
r.Get("/", app.findProjectByID)
|
||||||
|
r.Patch("/", app.updateProject)
|
||||||
|
r.Delete("/", app.deleteProject)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/statuses", func(r chi.Router) {
|
||||||
|
r.Use(app.AuthMiddleware)
|
||||||
|
r.Get("/", app.findStatuses)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/issues", func(r chi.Router) {
|
||||||
|
r.Use(app.AuthMiddleware)
|
||||||
|
r.Get("/", app.findIssues)
|
||||||
|
r.Post("/", app.createIssues)
|
||||||
|
r.Post("/positions", app.updatePositions)
|
||||||
|
|
||||||
|
r.Route("/{id}", func(r chi.Router) {
|
||||||
|
r.Get("/", app.findIssuesByID)
|
||||||
|
r.Delete("/", app.deleteIssues)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/preferences", func(r chi.Router) {
|
||||||
|
r.Use(app.AuthMiddleware)
|
||||||
|
r.Get("/", app.getSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/profile", func(r chi.Router) {
|
||||||
|
r.Use(app.AuthMiddleware)
|
||||||
|
r.Get("/", app.getProfile)
|
||||||
|
r.Patch("/", app.updateProfile)
|
||||||
|
r.Delete("/", app.deleteProfile)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/users", func(r chi.Router) {
|
||||||
|
r.Use(app.AuthMiddleware)
|
||||||
|
r.Get("/", app.findUsers)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dist, err := fs.Sub(web.DistDir, "dist")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := dist.Open(strings.TrimPrefix(r.URL.Path, "/"))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
r.URL.Path = "/"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defer func(file fs.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.FileServer(http.FS(dist)).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) Start(mux http.Handler) error {
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: "0.0.0.0:3000",
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.ListenAndServe()
|
||||||
|
}
|
||||||
78
internal/server/status.go
Normal file
78
internal/server/status.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"madsky.ru/go-tracker/internal/model/status"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
"madsky.ru/go-tracker/internal/server/request"
|
||||||
|
"madsky.ru/go-tracker/internal/server/response"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) findStatuses(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
u := ctx.Value(UserContext{}).(*user.User)
|
||||||
|
fmt.Println(u)
|
||||||
|
|
||||||
|
s, err := app.Storage.Status.Find(app.Ctx)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) FindStatusById(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := request.Param(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := app.Storage.Status.FindOne(app.Ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) CreateStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
|
||||||
|
var statusDto status.CreateStatusDTO
|
||||||
|
|
||||||
|
err := dec.Decode(&statusDto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusNotAcceptable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := app.Storage.Status.Create(app.Ctx, &statusDto)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, &s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) DeleteStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := request.Param(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := app.Storage.Status.Remove(app.Ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.WriteJSON(w, nil, http.StatusOK, count)
|
||||||
|
}
|
||||||
7
internal/server/user.go
Normal file
7
internal/server/user.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func (app *Application) findUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return
|
||||||
|
}
|
||||||
202
internal/storage/issues.go
Normal file
202
internal/storage/issues.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"madsky.ru/go-tracker/internal/database"
|
||||||
|
"madsky.ru/go-tracker/internal/model/issue"
|
||||||
|
"madsky.ru/go-tracker/internal/model/project"
|
||||||
|
"madsky.ru/go-tracker/internal/model/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueRepository interface {
|
||||||
|
Find(ctx context.Context) ([]*issue.Issue, error)
|
||||||
|
UpdatePositions(ctx context.Context, position int, statusId uint32, id uint32) (*uint32, error)
|
||||||
|
FindOne(ctx context.Context, id uint64) (*issue.Issue, error)
|
||||||
|
Create(ctx context.Context, dto *issue.CreateIssueDTO) (*uint64, error)
|
||||||
|
Update(ctx context.Context, id uint64, issue *issue.Issue) error
|
||||||
|
Remove(ctx context.Context, id uint64) (uint64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssueStore struct {
|
||||||
|
client database.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IssueStore) Find(ctx context.Context) ([]*issue.Issue, error) {
|
||||||
|
q := `select
|
||||||
|
i."id",
|
||||||
|
i."name",
|
||||||
|
i."description",
|
||||||
|
i."position",
|
||||||
|
i."created",
|
||||||
|
i."project_id",
|
||||||
|
i."status_id",
|
||||||
|
p."id" as project_id,
|
||||||
|
p."name" as project_name,
|
||||||
|
p."description" as project_description,
|
||||||
|
p."key" as project_key,
|
||||||
|
s."id" as status_id,
|
||||||
|
s."name" as status_name,
|
||||||
|
s."description" as status_description,
|
||||||
|
s."position" as position
|
||||||
|
from issues i
|
||||||
|
join projects p on p.id = project_id
|
||||||
|
join statuses s on s.id = status_id`
|
||||||
|
|
||||||
|
rows, err := is.client.Query(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("rows", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issues := make([]*issue.Issue, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var n issue.Issue
|
||||||
|
var p project.Project
|
||||||
|
var s status.Status
|
||||||
|
|
||||||
|
err = rows.Scan(
|
||||||
|
&n.ID,
|
||||||
|
&n.Name,
|
||||||
|
&n.Description,
|
||||||
|
&n.Position,
|
||||||
|
&n.Created,
|
||||||
|
&n.ProjectID,
|
||||||
|
&n.StatusID,
|
||||||
|
&p.ID,
|
||||||
|
&p.Name,
|
||||||
|
&p.Description,
|
||||||
|
&p.Key,
|
||||||
|
&s.ID,
|
||||||
|
&s.Name,
|
||||||
|
&s.Description,
|
||||||
|
&s.Position,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("scan", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n.Project = p
|
||||||
|
n.Status = s
|
||||||
|
issues = append(issues, &n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
log.Println("rows err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IssueStore) UpdatePositions(ctx context.Context, position int, statusId uint32, id uint32) (*uint32, error) {
|
||||||
|
q := `update issues set "position" = $1, "status_id"=$2 where "id"=$3 returning id`
|
||||||
|
|
||||||
|
var resultId uint32
|
||||||
|
|
||||||
|
if err := is.client.QueryRow(ctx, q, position, statusId, id).Scan(&resultId); err != nil {
|
||||||
|
fmt.Println(fmt.Sprintf("error %v", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resultId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IssueStore) FindOne(ctx context.Context, id uint64) (*issue.Issue, error) {
|
||||||
|
q := `select
|
||||||
|
i."id",
|
||||||
|
i."name",
|
||||||
|
i."description",
|
||||||
|
i."position",
|
||||||
|
i."created",
|
||||||
|
i."project_id",
|
||||||
|
i."status_id",
|
||||||
|
p."id" as project_id,
|
||||||
|
p."name" as project_name,
|
||||||
|
p."description" as project_description,
|
||||||
|
p."key" as project_key,
|
||||||
|
s."id" as status_id,
|
||||||
|
s."name" as status_name,
|
||||||
|
s."description" as status_description,
|
||||||
|
s."position" as position
|
||||||
|
from issues i
|
||||||
|
join projects p on p.id = project_id
|
||||||
|
join statuses s on s.id = status_id
|
||||||
|
where i."id" = $1`
|
||||||
|
|
||||||
|
var n issue.Issue
|
||||||
|
var p project.Project
|
||||||
|
var s status.Status
|
||||||
|
|
||||||
|
if err := is.client.QueryRow(ctx, q, id).Scan(
|
||||||
|
&n.ID,
|
||||||
|
&n.Name,
|
||||||
|
&n.Description,
|
||||||
|
&n.Position,
|
||||||
|
&n.Created,
|
||||||
|
&n.ProjectID,
|
||||||
|
&n.StatusID,
|
||||||
|
&p.ID,
|
||||||
|
&p.Name,
|
||||||
|
&p.Description,
|
||||||
|
&p.Key,
|
||||||
|
&s.ID,
|
||||||
|
&s.Name,
|
||||||
|
&s.Description,
|
||||||
|
&s.Position,
|
||||||
|
); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return &issue.Issue{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Project = p
|
||||||
|
n.Status = s
|
||||||
|
|
||||||
|
return &n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IssueStore) Create(ctx context.Context, dto *issue.CreateIssueDTO) (*uint64, error) {
|
||||||
|
q := `insert into issues (name, description, project_id, status_id, position)
|
||||||
|
values ($1, $2, $3, $4, $5)
|
||||||
|
returning id`
|
||||||
|
|
||||||
|
var position uint32 = 0
|
||||||
|
var id uint64
|
||||||
|
|
||||||
|
if dto.Position != nil {
|
||||||
|
position = *dto.Position
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := is.client.QueryRow(ctx, q, dto.Name, dto.Description, dto.ProjectID, dto.StatusID, position).Scan(&id); err != nil {
|
||||||
|
fmt.Println(fmt.Sprintf("error %v", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IssueStore) Update(ctx context.Context, id uint64, issue *issue.Issue) error {
|
||||||
|
//TODO implement me
|
||||||
|
fmt.Println("update", id, issue, ctx)
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *IssueStore) Remove(ctx context.Context, id uint64) (uint64, error) {
|
||||||
|
q := "delete from issues where id=$1"
|
||||||
|
|
||||||
|
tag, err := is.client.Exec(ctx, q, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("exec error", err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected := tag.RowsAffected()
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return 0, errors.New("project not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(rowsAffected), nil
|
||||||
|
}
|
||||||
169
internal/storage/projects.go
Normal file
169
internal/storage/projects.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"log"
|
||||||
|
"madsky.ru/go-tracker/internal/database"
|
||||||
|
"madsky.ru/go-tracker/internal/model/project"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectRepository interface {
|
||||||
|
Find(ctx context.Context, userId uint32, isAdmin bool, filter *project.FilterDTO) ([]*project.Project, error)
|
||||||
|
FindOne(ctx context.Context, projectId uint64, userId uint32, isAdmin bool) (*project.Project, error)
|
||||||
|
Create(ctx context.Context, dto *project.CreateProjectDTO) (*project.Project, error)
|
||||||
|
Update(ctx context.Context, id uint64, issue *project.UpdateProjectDTO) (*project.Project, error)
|
||||||
|
Remove(ctx context.Context, id uint64) (uint64, error)
|
||||||
|
}
|
||||||
|
type ProjectStore struct {
|
||||||
|
client database.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProjectStore) Find(ctx context.Context, userId uint32, isAdmin bool, filter *project.FilterDTO) ([]*project.Project, error) {
|
||||||
|
query := `select p.id, p.name, p.description, p.key from projects p `
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
if isAdmin == false {
|
||||||
|
query += `left join user_to_project up on up.project_id = p.id where up.user_id = $1`
|
||||||
|
args = append(args, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ps.client.Query(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ProjectStore rows", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// another variant
|
||||||
|
//projects, err := pgx.CollectRows(rows, pgx.RowToStructByName[project.Project])
|
||||||
|
|
||||||
|
projects := make([]*project.Project, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var p project.Project
|
||||||
|
|
||||||
|
err = rows.Scan(&p.ID, &p.Name, &p.Description, &p.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("scan", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
projects = append(projects, &p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
log.Println("rows err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProjectStore) FindOne(ctx context.Context, projectId uint64, userId uint32, isAdmin bool) (*project.Project, error) {
|
||||||
|
args := []interface{}{projectId}
|
||||||
|
|
||||||
|
query := `select p.id, p.name, p.description, p.key from projects p
|
||||||
|
left join user_to_project up on up.project_id = p.id
|
||||||
|
where p.id = $1 `
|
||||||
|
|
||||||
|
if isAdmin == false {
|
||||||
|
query += "and up.user_id = $2"
|
||||||
|
args = append(args, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var p project.Project
|
||||||
|
|
||||||
|
rows, _ := ps.client.Query(ctx, query, args...)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
p, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[project.Project])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CollectOneRow FindById project", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProjectStore) Create(ctx context.Context, dto *project.CreateProjectDTO) (*project.Project, error) {
|
||||||
|
//q := "insert into projects (name, description, key) values ($1, $2, $3) returning id, name, description, key"
|
||||||
|
query := `insert into projects (name, description, key)
|
||||||
|
values (@projName, @projDescription, @projKey)
|
||||||
|
returning id, name, description, key`
|
||||||
|
|
||||||
|
key := dto.Key
|
||||||
|
|
||||||
|
if dto.Key == "" {
|
||||||
|
key = trimString(dto.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"projName": dto.Name,
|
||||||
|
"projDescription": dto.Description,
|
||||||
|
"projKey": key,
|
||||||
|
}
|
||||||
|
|
||||||
|
//rows, _ := ps.client.Query(ctx, q, dto.Name, dto.Description, key)
|
||||||
|
rows, _ := ps.client.Query(ctx, query, args)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
p, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[project.Project])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CollectOneRow Create Project", err)
|
||||||
|
//return nil, fmt.Errorf("unable to insert row: %w", err)
|
||||||
|
return nil, fmt.Errorf("KEY must be 4 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProjectStore) Update(ctx context.Context, id uint64, dto *project.UpdateProjectDTO) (*project.Project, error) {
|
||||||
|
q := `update projects
|
||||||
|
set name = $1, description = $2
|
||||||
|
where id = $3
|
||||||
|
returning id, name, description, key`
|
||||||
|
|
||||||
|
var p project.Project
|
||||||
|
|
||||||
|
if err := ps.client.QueryRow(ctx, q, dto.Name, dto.Description, id).Scan(&p.ID, &p.Name, &p.Description, &p.Key); err != nil {
|
||||||
|
fmt.Println(fmt.Sprintf("error %v", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProjectStore) Remove(ctx context.Context, id uint64) (uint64, error) {
|
||||||
|
q := `delete from projects where id=$1`
|
||||||
|
|
||||||
|
tag, err := ps.client.Exec(ctx, q, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("exec error", err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected := tag.RowsAffected()
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return 0, errors.New("project not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(rowsAffected), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimString(str string) string {
|
||||||
|
runeStr := []rune(str)
|
||||||
|
var res string
|
||||||
|
|
||||||
|
if len(runeStr) > 4 {
|
||||||
|
res = string(runeStr[:3])
|
||||||
|
} else {
|
||||||
|
res = string(runeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(res)
|
||||||
|
}
|
||||||
46
internal/storage/server-settings.go
Normal file
46
internal/storage/server-settings.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"madsky.ru/go-tracker/internal/database"
|
||||||
|
"madsky.ru/go-tracker/internal/model/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerSettingsRepository interface {
|
||||||
|
Find(ctx context.Context) ([]*setting.Setting, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerSettingsStore struct {
|
||||||
|
client database.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *ServerSettingsStore) Find(ctx context.Context) ([]*setting.Setting, error) {
|
||||||
|
query := "select id, name, value from public.server_settings"
|
||||||
|
|
||||||
|
rows, err := store.client.Query(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ServerSettingsStore query err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]*setting.Setting, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var r setting.Setting
|
||||||
|
|
||||||
|
err := rows.Scan(&r.ID, &r.Name, &r.Value)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ServerSettingsStore scan err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res = append(res, &r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
log.Println("ServerSettingsStore rows err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
99
internal/storage/status.go
Normal file
99
internal/storage/status.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"madsky.ru/go-tracker/internal/database"
|
||||||
|
"madsky.ru/go-tracker/internal/model/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusRepository interface {
|
||||||
|
Find(ctx context.Context) ([]*status.Status, error)
|
||||||
|
FindOne(ctx context.Context, id uint64) (*status.Status, error)
|
||||||
|
Create(ctx context.Context, dto *status.CreateStatusDTO) (*status.Status, error)
|
||||||
|
Update(ctx context.Context, id uint64, issue *status.Status) error
|
||||||
|
Remove(ctx context.Context, id uint64) (uint64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusStore struct {
|
||||||
|
client database.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusStore) Find(ctx context.Context) ([]*status.Status, error) {
|
||||||
|
query := "select id, name, description, position from statuses"
|
||||||
|
|
||||||
|
rows, err := r.client.Query(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("rows", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses := make([]*status.Status, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var n status.Status
|
||||||
|
|
||||||
|
err = rows.Scan(&n.ID, &n.Name, &n.Description, &n.Position)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("scan", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
statuses = append(statuses, &n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
log.Println("rows err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusStore) FindOne(ctx context.Context, id uint64) (*status.Status, error) {
|
||||||
|
query := "select id, name, description, position from statuses where id = $1"
|
||||||
|
|
||||||
|
var s status.Status
|
||||||
|
if err := r.client.QueryRow(ctx, query, id).Scan(&s.ID, &s.Name, &s.Description, &s.Position); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusStore) Create(ctx context.Context, dto *status.CreateStatusDTO) (*status.Status, error) {
|
||||||
|
q := "insert into statuses (name, description) values ($1, $2) returning id, name, description, position"
|
||||||
|
|
||||||
|
var s status.Status
|
||||||
|
|
||||||
|
if err := r.client.QueryRow(ctx, q, dto.Name, dto.Description).Scan(&s.ID, &s.Name, &s.Description, &s.Position); err != nil {
|
||||||
|
fmt.Println(fmt.Sprintf("error %v", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusStore) Update(ctx context.Context, id uint64, issue *status.Status) error {
|
||||||
|
//TODO implement me
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusStore) Remove(ctx context.Context, id uint64) (uint64, error) {
|
||||||
|
q := "delete from statuses where id=$1"
|
||||||
|
|
||||||
|
tag, err := r.client.Exec(ctx, q, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("exec error", err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected := tag.RowsAffected()
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return 0, errors.New("status not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(rowsAffected), nil
|
||||||
|
}
|
||||||
25
internal/storage/storage.go
Normal file
25
internal/storage/storage.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
Projects ProjectRepository
|
||||||
|
ServerSettings ServerSettingsRepository
|
||||||
|
Issues IssueRepository
|
||||||
|
Status StatusRepository
|
||||||
|
User UserRepository
|
||||||
|
UserToProject UserToProjectRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(client *pgxpool.Pool) *Storage {
|
||||||
|
return &Storage{
|
||||||
|
Projects: &ProjectStore{client: client},
|
||||||
|
ServerSettings: &ServerSettingsStore{client: client},
|
||||||
|
Issues: &IssueStore{client: client},
|
||||||
|
Status: &StatusStore{client: client},
|
||||||
|
User: &UserStore{client: client},
|
||||||
|
UserToProject: &UserToProjectStore{client: client},
|
||||||
|
}
|
||||||
|
}
|
||||||
32
internal/storage/user-to-project.go
Normal file
32
internal/storage/user-to-project.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"madsky.ru/go-tracker/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserToProjectRepository interface {
|
||||||
|
Create(ctx context.Context, userId uint32, projectId uint32) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserToProjectStore struct {
|
||||||
|
client database.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (up *UserToProjectStore) Create(ctx context.Context, userId uint32, projectId uint32) error {
|
||||||
|
query := `insert into user_to_project (user_id, project_id) values (@userId, @projectId)`
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"userId": userId,
|
||||||
|
"projectId": projectId,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := up.client.Exec(ctx, query, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to insert row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
108
internal/storage/user.go
Normal file
108
internal/storage/user.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"madsky.ru/go-tracker/internal/database"
|
||||||
|
"madsky.ru/go-tracker/internal/model/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
FindByEmail(ctx context.Context, email string) (*user.User, error)
|
||||||
|
FindById(ctx context.Context, id uint32) (*user.User, error)
|
||||||
|
Create(ctx context.Context, user *user.User) (*user.User, error)
|
||||||
|
IsEmpty(ctx context.Context) (bool, error)
|
||||||
|
Update(ctx context.Context, id uint32, dto *user.UpdateUserDTO) (*user.User, error)
|
||||||
|
//Find(ctx context.Context) ([]*user.User, error)
|
||||||
|
//FindOne(ctx context.Context, id uint64) (*user.User, error)
|
||||||
|
//Create(ctx context.Context, dto *user.CreateUserDTO) (*user.User, error)
|
||||||
|
//Remove(ctx context.Context, id uint64) (uint64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserStore struct {
|
||||||
|
client database.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *UserStore) FindById(ctx context.Context, id uint32) (*user.User, error) {
|
||||||
|
query := `select id, email, name, password_hash, role
|
||||||
|
from users
|
||||||
|
where id = $1`
|
||||||
|
|
||||||
|
rows, _ := us.client.Query(ctx, query, id)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CollectOneRow FindById User", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *UserStore) FindByEmail(ctx context.Context, email string) (*user.User, error) {
|
||||||
|
query := `select id, email, name, password_hash, role
|
||||||
|
from users
|
||||||
|
where email = $1`
|
||||||
|
|
||||||
|
rows, _ := us.client.Query(ctx, query, email)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CollectOneRow FindByEmail User", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *UserStore) IsEmpty(ctx context.Context) (bool, error) {
|
||||||
|
query := "select count(id) from users limit 1"
|
||||||
|
|
||||||
|
rows, _ := us.client.Query(ctx, query)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
count, err := pgx.CollectOneRow(rows, pgx.RowTo[int])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CollectOneRow IsEmpty User", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *UserStore) Create(ctx context.Context, dto *user.User) (*user.User, error) {
|
||||||
|
query := `insert into users (email, name, password_hash, role)
|
||||||
|
values ($1, $2, $3, $4)
|
||||||
|
returning id, name, email, password_hash, role`
|
||||||
|
rows, _ := us.client.Query(ctx, query, dto.Email, dto.Name, dto.PasswordHash, dto.Role)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CollectOneRow Create User", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us *UserStore) Update(ctx context.Context, id uint32, dto *user.UpdateUserDTO) (*user.User, error) {
|
||||||
|
query := `update users
|
||||||
|
set name = $1
|
||||||
|
where id = $2
|
||||||
|
returning id, email, name, password_hash, role`
|
||||||
|
|
||||||
|
rows, _ := us.client.Query(ctx, query, dto.Name, id)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CollectOneRow Update User", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
180
proto/hello.pb.go
Normal file
180
proto/hello.pb.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.5
|
||||||
|
// protoc v6.30.0--rc2
|
||||||
|
// source: proto/hello.proto
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelloRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HelloRequest) Reset() {
|
||||||
|
*x = HelloRequest{}
|
||||||
|
mi := &file_proto_hello_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HelloRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HelloRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *HelloRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_hello_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*HelloRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_hello_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HelloRequest) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelloResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HelloResponse) Reset() {
|
||||||
|
*x = HelloResponse{}
|
||||||
|
mi := &file_proto_hello_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HelloResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HelloResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *HelloResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_hello_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*HelloResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_hello_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HelloResponse) GetMessage() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_proto_hello_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_proto_hello_proto_rawDesc = string([]byte{
|
||||||
|
0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72,
|
||||||
|
0x6f, 0x74, 0x6f, 0x12, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65,
|
||||||
|
0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
|
||||||
|
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x29,
|
||||||
|
0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
|
0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x40, 0x0a, 0x05, 0x48, 0x65, 0x6c,
|
||||||
|
0x6c, 0x6f, 0x12, 0x37, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x13,
|
||||||
|
0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75,
|
||||||
|
0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c,
|
||||||
|
0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x6d,
|
||||||
|
0x61, 0x64, 0x73, 0x6b, 0x79, 0x2e, 0x72, 0x75, 0x2f, 0x67, 0x6f, 0x2d, 0x66, 0x69, 0x6e, 0x61,
|
||||||
|
0x6e, 0x63, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x33,
|
||||||
|
})
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_proto_hello_proto_rawDescOnce sync.Once
|
||||||
|
file_proto_hello_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_proto_hello_proto_rawDescGZIP() []byte {
|
||||||
|
file_proto_hello_proto_rawDescOnce.Do(func() {
|
||||||
|
file_proto_hello_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_hello_proto_rawDesc), len(file_proto_hello_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_proto_hello_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_proto_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
|
var file_proto_hello_proto_goTypes = []any{
|
||||||
|
(*HelloRequest)(nil), // 0: hello.HelloRequest
|
||||||
|
(*HelloResponse)(nil), // 1: hello.HelloResponse
|
||||||
|
}
|
||||||
|
var file_proto_hello_proto_depIdxs = []int32{
|
||||||
|
0, // 0: hello.Hello.SayHello:input_type -> hello.HelloRequest
|
||||||
|
1, // 1: hello.Hello.SayHello:output_type -> hello.HelloResponse
|
||||||
|
1, // [1:2] is the sub-list for method output_type
|
||||||
|
0, // [0:1] is the sub-list for method input_type
|
||||||
|
0, // [0:0] is the sub-list for extension type_name
|
||||||
|
0, // [0:0] is the sub-list for extension extendee
|
||||||
|
0, // [0:0] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_proto_hello_proto_init() }
|
||||||
|
func file_proto_hello_proto_init() {
|
||||||
|
if File_proto_hello_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_hello_proto_rawDesc), len(file_proto_hello_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 2,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_proto_hello_proto_goTypes,
|
||||||
|
DependencyIndexes: file_proto_hello_proto_depIdxs,
|
||||||
|
MessageInfos: file_proto_hello_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_proto_hello_proto = out.File
|
||||||
|
file_proto_hello_proto_goTypes = nil
|
||||||
|
file_proto_hello_proto_depIdxs = nil
|
||||||
|
}
|
||||||
17
proto/hello.proto
Normal file
17
proto/hello.proto
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
syntax="proto3";
|
||||||
|
|
||||||
|
package hello;
|
||||||
|
|
||||||
|
option go_package = "madsky.ru/go-tracker/proto";
|
||||||
|
|
||||||
|
service Hello {
|
||||||
|
rpc SayHello (HelloRequest) returns (HelloResponse) {};
|
||||||
|
}
|
||||||
|
|
||||||
|
message HelloRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HelloResponse {
|
||||||
|
string message = 1;
|
||||||
|
}
|
||||||
121
proto/hello_grpc.pb.go
Normal file
121
proto/hello_grpc.pb.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
|
// - protoc v6.30.0--rc2
|
||||||
|
// source: proto/hello.proto
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
Hello_SayHello_FullMethodName = "/hello.Hello/SayHello"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HelloClient is the client API for Hello service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type HelloClient interface {
|
||||||
|
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type helloClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHelloClient(cc grpc.ClientConnInterface) HelloClient {
|
||||||
|
return &helloClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *helloClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(HelloResponse)
|
||||||
|
err := c.cc.Invoke(ctx, Hello_SayHello_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HelloServer is the server API for Hello service.
|
||||||
|
// All implementations must embed UnimplementedHelloServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type HelloServer interface {
|
||||||
|
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
|
||||||
|
mustEmbedUnimplementedHelloServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedHelloServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedHelloServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedHelloServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedHelloServer) mustEmbedUnimplementedHelloServer() {}
|
||||||
|
func (UnimplementedHelloServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeHelloServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to HelloServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeHelloServer interface {
|
||||||
|
mustEmbedUnimplementedHelloServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterHelloServer(s grpc.ServiceRegistrar, srv HelloServer) {
|
||||||
|
// If the following call pancis, it indicates UnimplementedHelloServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&Hello_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Hello_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(HelloRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(HelloServer).SayHello(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Hello_SayHello_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(HelloServer).SayHello(ctx, req.(*HelloRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hello_ServiceDesc is the grpc.ServiceDesc for Hello service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var Hello_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "hello.Hello",
|
||||||
|
HandlerType: (*HelloServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "SayHello",
|
||||||
|
Handler: _Hello_SayHello_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/hello.proto",
|
||||||
|
}
|
||||||
8
sql/down.sql
Normal file
8
sql/down.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
drop table server_settings;
|
||||||
|
drop table user_to_issue;
|
||||||
|
drop table user_to_project;
|
||||||
|
drop table categories;
|
||||||
|
drop table issues;
|
||||||
|
drop table users;
|
||||||
|
drop table projects;
|
||||||
|
drop table statuses;
|
||||||
100
sql/up.sql
Normal file
100
sql/up.sql
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
-- settings
|
||||||
|
create table if not exists server_settings
|
||||||
|
(
|
||||||
|
id integer primary key generated by default as identity,
|
||||||
|
name varchar not null,
|
||||||
|
value varchar not null
|
||||||
|
);
|
||||||
|
insert into server_settings (name, value)
|
||||||
|
values ('version', '1.0.0');
|
||||||
|
|
||||||
|
-- projects
|
||||||
|
create table if not exists projects
|
||||||
|
(
|
||||||
|
id integer primary key generated by default as identity,
|
||||||
|
name varchar not null,
|
||||||
|
description varchar,
|
||||||
|
key varchar(4) not null
|
||||||
|
);
|
||||||
|
insert into projects (name, key)
|
||||||
|
values ('demo', 'DEMO'),('madsky', 'MAD');
|
||||||
|
|
||||||
|
-- statuses
|
||||||
|
create table if not exists statuses
|
||||||
|
(
|
||||||
|
id integer primary key generated by default as identity,
|
||||||
|
name varchar not null,
|
||||||
|
description varchar,
|
||||||
|
position integer default 0 not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into statuses (name)
|
||||||
|
values ('todo'),
|
||||||
|
('is working'),
|
||||||
|
('done');
|
||||||
|
|
||||||
|
-- issues
|
||||||
|
create table if not exists issues
|
||||||
|
(
|
||||||
|
id integer primary key generated by default as identity,
|
||||||
|
name varchar not null,
|
||||||
|
description varchar,
|
||||||
|
position integer default 0 not null,
|
||||||
|
created timestamptz DEFAULT now() not null,
|
||||||
|
status_id integer not null,
|
||||||
|
project_id integer not null,
|
||||||
|
foreign key (status_id) references statuses,
|
||||||
|
foreign key (project_id) references projects
|
||||||
|
);
|
||||||
|
|
||||||
|
-- users
|
||||||
|
create table if not exists users
|
||||||
|
(
|
||||||
|
id integer primary key generated by default as identity,
|
||||||
|
email varchar not null unique,
|
||||||
|
password_hash varchar,
|
||||||
|
name varchar,
|
||||||
|
avatar varchar,
|
||||||
|
role varchar default 'user'
|
||||||
|
);
|
||||||
|
create index if not exists user_email_index on users (email);
|
||||||
|
-- insert into users (email, password_hash, name, isAdmin)
|
||||||
|
-- values ('admin@admin.ru', '', 'admin', true);
|
||||||
|
|
||||||
|
-- relation
|
||||||
|
create table if not exists user_to_issue
|
||||||
|
(
|
||||||
|
user_id integer,
|
||||||
|
issue_id integer,
|
||||||
|
foreign key (user_id) references users,
|
||||||
|
foreign key (issue_id) references issues on delete cascade,
|
||||||
|
constraint user_to_issue_primary_key primary key (user_id, issue_id)
|
||||||
|
);
|
||||||
|
create index if not exists user_to_issue_user_id_index on user_to_issue (user_id);
|
||||||
|
create index if not exists user_to_issue_issue_id_index on user_to_issue (issue_id);
|
||||||
|
|
||||||
|
create table if not exists user_to_project
|
||||||
|
(
|
||||||
|
user_id integer,
|
||||||
|
project_id integer,
|
||||||
|
foreign key (user_id) references users,
|
||||||
|
foreign key (project_id) references projects on delete cascade,
|
||||||
|
constraint user_to_project_primary_key primary key (user_id, project_id)
|
||||||
|
);
|
||||||
|
create index if not exists user_to_project_user_id_index on user_to_project (user_id);
|
||||||
|
create index if not exists user_to_project_project_id_index on user_to_project (project_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- categories
|
||||||
|
create table if not exists categories
|
||||||
|
(
|
||||||
|
id integer primary key generated by default as identity,
|
||||||
|
name varchar not null,
|
||||||
|
description varchar,
|
||||||
|
color varchar(7) default '#000000',
|
||||||
|
image varchar,
|
||||||
|
user_id integer not null,
|
||||||
|
foreign key (user_id) references users
|
||||||
|
);
|
||||||
|
-- insert into categories (name, color, user_id)
|
||||||
|
-- values ('Default', '#ffffff', 1);
|
||||||
Reference in New Issue
Block a user