This commit is contained in:
2025-11-12 09:41:52 +03:00
commit 2a8566712a
44 changed files with 2602 additions and 0 deletions

150
internal/server/auth.go Normal file
View 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(&registerDto); 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)
}

View 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)
}

View 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
View 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)
}

View 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)
})
}

View 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
View 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)
}

View 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
}

View 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
}

View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
package server
import "net/http"
func (app *Application) findUsers(w http.ResponseWriter, r *http.Request) {
return
}