init
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user