init
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.tsbuildinfo
|
||||
/bin/
|
||||
/config/config.yaml
|
||||
@@ -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:
|
||||
CGO_ENABLED=0 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
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
app_name: 'APP'
|
||||
nats:
|
||||
host: ''
|
||||
token: ''
|
||||
database:
|
||||
host: ''
|
||||
port: 5432
|
||||
user: ''
|
||||
password: ''
|
||||
name: 'tracker'
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,108 @@
|
||||
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.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.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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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.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-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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,10 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
func NewLogger() *slog.Logger {
|
||||
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package setting
|
||||
|
||||
type Setting struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (app *Application) findUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
import{d as f,h as s,w as n,b as e,r as o,c as l,F as r,A as b,p as _,o as c,e as g,t as h}from"./index-KpQc9ZlS.js";const D=f({__name:"DynamicMenu",props:{menu:{},itemId:{},isHovering:{type:[Boolean,null]}},setup(m){const i=m;return(B,C)=>{const a=o("v-icon"),d=o("v-btn"),p=o("v-divider"),v=o("v-list-item-title"),u=o("v-list-item"),k=o("v-list"),y=o("v-menu");return c(),s(y,_(i,{width:"200"}),{activator:n(({props:t})=>[e(d,_({icon:"",rounded:"",variant:i.isHovering?"tonal":"text"},t),{default:n(()=>[e(a,{icon:"mdi-dots-horizontal"})]),_:2},1040,["variant"])]),default:n(()=>[e(k,{density:"compact",class:"mt-1"},{default:n(()=>[(c(!0),l(r,null,b(i.menu,t=>(c(),l(r,{key:t.id},[t.type==="divider"?(c(),s(p,{key:0,class:"my-1"})):(c(),s(u,{key:1,disabled:t.disabled,color:t.color,onClick:x=>t.click&&t.click(i.itemId)},{default:n(()=>[e(v,null,{default:n(()=>[e(a,{class:"mr-2",color:t.color,icon:t.icon},null,8,["color","icon"]),g("span",null,h(t.title),1)]),_:2},1024)]),_:2},1032,["disabled","color","onClick"]))],64))),128))]),_:1})]),_:1},16)}}});export{D as _};
|
||||
+1
@@ -0,0 +1 @@
|
||||
.sortable-ghost[data-v-1c47e356]{opacity:.33;transition:opacity .3s}.sortable-drag[data-v-1c47e356]{background-color:rgb(var(--v-theme-background))}.v-tabs .v-tabs-slider-wrapper[data-v-93eecf16]{transition:none}
|
||||
+10
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+5
File diff suppressed because one or more lines are too long
Vendored
+58
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/assets/index-KpQc9ZlS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsusvx-X.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed all:dist
|
||||
var DistDir embed.FS
|
||||
Reference in New Issue
Block a user