Merge pull request #1 from anxi0uz/feature/back-01

First swagger version, added auto-migrate on startup
This commit is contained in:
2026-03-15 20:41:35 +05:00
committed by GitHub
18 changed files with 984 additions and 30 deletions

15
.env
View File

@@ -0,0 +1,15 @@
LOGIFLOW_DATABASE_HOST=postgres
LOGIFLOW_DATABASE_PORT=5432
LOGIFLOW_DATABASE_USER=postgres
LOGIFLOW_DATABASE_PASSWORD=pass1234
LOGIFLOW_DATABASE_NAME=logiflow
LOGIFLOW_DATABASE_SSLMODE=disable
LOGIFLOW_REDIS_ADDR=redis:6379
LOGIFLOW_REDIS_PASSWORD=redis123
LOGIFLOW_JWT_KEY=98c5772ae16aaa4fd0013eb338252a93b198fb40e9337506334b3aeb21abbe4cd9289cdd
GOOSE_DRIVER=postgres
GOOSE_DBSTRING=postgres://${LOGIFLOW_DATABASE_USER}:${LOGIFLOW_DATABASE_PASSWORD}@localhost:${LOGIFLOW_DATABASE_PORT}/${LOGIFLOW_DATABASE_NAME}?sslmode=disable
GOOSE_MIGRATION_DIR=./migrations
GOOSE_TABLE=goose_migrations

40
cmd/main.go Normal file
View File

@@ -0,0 +1,40 @@
package main
import (
"context"
"log/slog"
"os"
"github.com/anxi0uz/logiflow/internal/config"
"github.com/golang-cz/devslog"
)
func NewDevLogger() {
opts := &devslog.Options{
MaxSlicePrintSize: 4,
SortKeys: true,
TimeFormat: "15:04:05.000",
NewLineAfterLog: true,
DebugColor: devslog.Cyan,
StringerFormatter: true,
}
handler := devslog.NewHandler(os.Stdout, opts)
logger := slog.New(handler)
slog.SetDefault(logger)
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
NewDevLogger()
cfg, err := config.NewConfig(ctx, "configs/config.toml")
if err != nil {
slog.ErrorContext(ctx, "Cant load configs", slog.String("Error", err.Error()))
}
slog.SetLogLoggerLevel(cfg.Logiflow.LogLevel)
}

12
go.mod
View File

@@ -3,6 +3,8 @@ module github.com/anxi0uz/logiflow
go 1.25.7
require (
github.com/go-chi/chi/v5 v5.2.5
github.com/golang-cz/devslog v0.0.15
github.com/huandu/go-sqlbuilder v1.39.1
github.com/jackc/pgx/v5 v5.8.0
github.com/joho/godotenv v1.5.1
@@ -10,6 +12,8 @@ require (
github.com/knadh/koanf/providers/env v1.1.0
github.com/knadh/koanf/providers/file v1.2.1
github.com/knadh/koanf/v2 v2.3.2
github.com/oapi-codegen/runtime v1.2.0
github.com/pressly/goose/v3 v3.27.0
github.com/redis/go-redis/v9 v9.18.0
)
@@ -18,17 +22,21 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/go-clone v1.7.3 // indirect
github.com/huandu/xstrings v1.4.0 // 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/knadh/koanf/maps v0.1.2 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
)

46
go.sum
View File

@@ -9,10 +9,18 @@ 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-cz/devslog v0.0.15 h1:ejoBLTCwJHWGbAmDf2fyTJJQO3AkzcPjw8SC9LaOQMI=
github.com/golang-cz/devslog v0.0.15/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8=
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/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
@@ -32,8 +40,8 @@ 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/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
@@ -44,16 +52,30 @@ github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4=
github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4=
github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
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/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -64,14 +86,26 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=

View File

@@ -1,5 +1,345 @@
openapi: 3.0.3
info:
title: logiflow
description: API для информационной системы для логистической компании
version: 1.0.0
title: Logiflow API
description: API для логистической информационной системы
version: 0.1.0
contact:
name: anxi0uz
servers:
- url: http://localhost:3001
description: Local development
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
ApiResponse:
type: object
properties:
status:
type: integer
nullable: false
data:
type: object
nullable: true
success:
type: boolean
nullable: false
requestID:
type: string
nullable: false
ErrorResponse:
type: object
properties:
status:
type: integer
message:
type: string
requestID:
type: string
User:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
format: email
slug:
type: string
fullName:
type: string
nullable: true
avatarUrl:
type: string
nullable: true
passwordHash:
type: string
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
nullable: true
lastLoginAt:
type: string
format: date-time
nullable: true
UserResponse:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
format: email
slug:
type: string
fullName:
type: string
nullable: true
avatarUrl:
type: string
nullable: true
createdAt:
type: string
format: date-time
RegisterRequest:
type: object
required: [email, password, fullName]
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
fullName:
type: string
LoginRequest:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
TokenRefreshRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
description: Refresh токен, полученный при логине
TokenResponse:
type: object
required: [accessToken, refreshToken]
properties:
accessToken:
type: string
description: Access токен (JWT)
refreshToken:
type: string
description: Refresh токен (opaque token, rotation)
expiresIn:
type: integer
description: Время жизни access токена в секундах
UserUpdate:
type: object
properties:
fullName:
type: string
minLength: 2
maxLength: 150
nullable: true
avatarUrl:
type: string
format: uri
nullable: true
password:
type: string
minLength: 8
description: Новый пароль (если меняется)
currentPassword:
type: string
minLength: 8
description: Текущий пароль (обязателен при смене пароля)
UserDeleteRequest:
type: object
required: [password]
properties:
password:
type: string
description: Текущий пароль для подтверждения удаления
paths:
/auth/register:
post:
operationId: authRegister
summary: Регистрация нового пользователя
tags: [Auth]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegisterRequest"
responses:
"201":
description: Пользователь успешно создан
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"400":
description: Некорректные данные
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Email уже занят
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/auth/login:
post:
operationId: authLogin
summary: Авторизация пользователя
tags: [Auth]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LoginRequest"
responses:
"200":
description: Успешный вход
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"401":
description: Неверный email или пароль
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/auth/refresh:
post:
operationId: authRefresh
summary: Обновление access-токена через refresh-токен
tags: [Auth]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TokenRefreshRequest"
responses:
"200":
description: Токены успешно обновлены
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"401":
description: Недействительный или истёкший refresh-токен
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"400":
description: Некорректный запрос
/auth/logout:
post:
operationId: authLogout
summary: Выход из системы
tags: [Auth]
security:
- BearerAuth: []
responses:
"204":
description: Успешный выход
"401":
description: Не авторизован
/me:
get:
operationId: getMe
summary: Получить текущего пользователя
tags: [Me]
security:
- BearerAuth: []
responses:
"200":
description: Данные пользователя
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"401":
description: Не авторизован
patch:
operationId: updateMe
summary: Обновить данные текущего пользователя
tags: [Me]
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserUpdate"
responses:
"200":
description: Пользователь обновлён
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"400":
description: Некорректные данные
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
description: Не авторизован
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
delete:
operationId: deleteMe
summary: Удалить аккаунт
tags: [Me]
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserDeleteRequest"
responses:
"204":
description: Аккаунт удалён
"400":
description: Неверный пароль
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
description: Не авторизован
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"

424
internal/api/gen.go Normal file
View File

@@ -0,0 +1,424 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT.
package api
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
openapi_types "github.com/oapi-codegen/runtime/types"
)
const (
BearerAuthScopes = "BearerAuth.Scopes"
)
// ApiResponse defines model for ApiResponse.
type ApiResponse struct {
Data *map[string]interface{} `json:"data,omitempty"`
RequestID *string `json:"requestID,omitempty"`
Status *int `json:"status,omitempty"`
Success *bool `json:"success,omitempty"`
}
// ErrorResponse defines model for ErrorResponse.
type ErrorResponse struct {
Message *string `json:"message,omitempty"`
RequestID *string `json:"requestID,omitempty"`
Status *int `json:"status,omitempty"`
}
// LoginRequest defines model for LoginRequest.
type LoginRequest struct {
Email openapi_types.Email `json:"email"`
Password string `json:"password"`
}
// RegisterRequest defines model for RegisterRequest.
type RegisterRequest struct {
Email openapi_types.Email `json:"email"`
FullName string `json:"fullName"`
Password string `json:"password"`
}
// TokenRefreshRequest defines model for TokenRefreshRequest.
type TokenRefreshRequest struct {
// RefreshToken Refresh токен, полученный при логине
RefreshToken string `json:"refreshToken"`
}
// UserDeleteRequest defines model for UserDeleteRequest.
type UserDeleteRequest struct {
// Password Текущий пароль для подтверждения удаления
Password string `json:"password"`
}
// UserUpdate defines model for UserUpdate.
type UserUpdate struct {
AvatarUrl *string `json:"avatarUrl,omitempty"`
// CurrentPassword Текущий пароль (обязателен при смене пароля)
CurrentPassword *string `json:"currentPassword,omitempty"`
FullName *string `json:"fullName,omitempty"`
// Password Новый пароль (если меняется)
Password *string `json:"password,omitempty"`
}
// AuthLoginJSONRequestBody defines body for AuthLogin for application/json ContentType.
type AuthLoginJSONRequestBody = LoginRequest
// AuthRefreshJSONRequestBody defines body for AuthRefresh for application/json ContentType.
type AuthRefreshJSONRequestBody = TokenRefreshRequest
// AuthRegisterJSONRequestBody defines body for AuthRegister for application/json ContentType.
type AuthRegisterJSONRequestBody = RegisterRequest
// DeleteMeJSONRequestBody defines body for DeleteMe for application/json ContentType.
type DeleteMeJSONRequestBody = UserDeleteRequest
// UpdateMeJSONRequestBody defines body for UpdateMe for application/json ContentType.
type UpdateMeJSONRequestBody = UserUpdate
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Авторизация пользователя
// (POST /auth/login)
AuthLogin(w http.ResponseWriter, r *http.Request)
// Выход из системы
// (POST /auth/logout)
AuthLogout(w http.ResponseWriter, r *http.Request)
// Обновление access-токена через refresh-токен
// (POST /auth/refresh)
AuthRefresh(w http.ResponseWriter, r *http.Request)
// Регистрация нового пользователя
// (POST /auth/register)
AuthRegister(w http.ResponseWriter, r *http.Request)
// Удалить аккаунт
// (DELETE /me)
DeleteMe(w http.ResponseWriter, r *http.Request)
// Получить текущего пользователя
// (GET /me)
GetMe(w http.ResponseWriter, r *http.Request)
// Обновить данные текущего пользователя
// (PATCH /me)
UpdateMe(w http.ResponseWriter, r *http.Request)
}
// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint.
type Unimplemented struct{}
// Авторизация пользователя
// (POST /auth/login)
func (_ Unimplemented) AuthLogin(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Выход из системы
// (POST /auth/logout)
func (_ Unimplemented) AuthLogout(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Обновление access-токена через refresh-токен
// (POST /auth/refresh)
func (_ Unimplemented) AuthRefresh(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Регистрация нового пользователя
// (POST /auth/register)
func (_ Unimplemented) AuthRegister(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Удалить аккаунт
// (DELETE /me)
func (_ Unimplemented) DeleteMe(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Получить текущего пользователя
// (GET /me)
func (_ Unimplemented) GetMe(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Обновить данные текущего пользователя
// (PATCH /me)
func (_ Unimplemented) UpdateMe(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
type MiddlewareFunc func(http.Handler) http.Handler
// AuthLogin operation middleware
func (siw *ServerInterfaceWrapper) AuthLogin(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.AuthLogin(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// AuthLogout operation middleware
func (siw *ServerInterfaceWrapper) AuthLogout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
r = r.WithContext(ctx)
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.AuthLogout(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// AuthRefresh operation middleware
func (siw *ServerInterfaceWrapper) AuthRefresh(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.AuthRefresh(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// AuthRegister operation middleware
func (siw *ServerInterfaceWrapper) AuthRegister(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.AuthRegister(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// DeleteMe operation middleware
func (siw *ServerInterfaceWrapper) DeleteMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
r = r.WithContext(ctx)
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.DeleteMe(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// GetMe operation middleware
func (siw *ServerInterfaceWrapper) GetMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
r = r.WithContext(ctx)
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetMe(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// UpdateMe operation middleware
func (siw *ServerInterfaceWrapper) UpdateMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
r = r.WithContext(ctx)
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.UpdateMe(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
type UnescapedCookieParamError struct {
ParamName string
Err error
}
func (e *UnescapedCookieParamError) Error() string {
return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName)
}
func (e *UnescapedCookieParamError) Unwrap() error {
return e.Err
}
type UnmarshalingParamError struct {
ParamName string
Err error
}
func (e *UnmarshalingParamError) Error() string {
return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error())
}
func (e *UnmarshalingParamError) Unwrap() error {
return e.Err
}
type RequiredParamError struct {
ParamName string
}
func (e *RequiredParamError) Error() string {
return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName)
}
type RequiredHeaderError struct {
ParamName string
Err error
}
func (e *RequiredHeaderError) Error() string {
return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName)
}
func (e *RequiredHeaderError) Unwrap() error {
return e.Err
}
type InvalidParamFormatError struct {
ParamName string
Err error
}
func (e *InvalidParamFormatError) Error() string {
return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error())
}
func (e *InvalidParamFormatError) Unwrap() error {
return e.Err
}
type TooManyValuesForParamError struct {
ParamName string
Count int
}
func (e *TooManyValuesForParamError) Error() string {
return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count)
}
// Handler creates http.Handler with routing matching OpenAPI spec.
func Handler(si ServerInterface) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{})
}
type ChiServerOptions struct {
BaseURL string
BaseRouter chi.Router
Middlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseRouter: r,
})
}
func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseURL: baseURL,
BaseRouter: r,
})
}
// HandlerWithOptions creates http.Handler with additional options
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
r := options.BaseRouter
if r == nil {
r = chi.NewRouter()
}
if options.ErrorHandlerFunc == nil {
options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
ErrorHandlerFunc: options.ErrorHandlerFunc,
}
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/auth/login", wrapper.AuthLogin)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/auth/logout", wrapper.AuthLogout)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/auth/refresh", wrapper.AuthRefresh)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/auth/register", wrapper.AuthRegister)
})
r.Group(func(r chi.Router) {
r.Delete(options.BaseURL+"/me", wrapper.DeleteMe)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/me", wrapper.GetMe)
})
r.Group(func(r chi.Router) {
r.Patch(options.BaseURL+"/me", wrapper.UpdateMe)
})
return r
}

View File

@@ -66,8 +66,8 @@ func NewConfig(ctx context.Context, configPath string) (*Config, error) {
k := koanf.New(".")
if err := k.Load(env.Provider("HANDBOOKS_", ".", func(s string) string {
return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(s, "HANDBOOKS_")), "_", ".")
if err := k.Load(env.Provider("LOGIFLOW_", ".", func(s string) string {
return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(s, "LOGIFLOW_")), "_", ".")
}), nil); err != nil {
return nil, fmt.Errorf("ошибка загрузки ENV: %w", err)
}

View File

@@ -2,9 +2,18 @@ package database
import (
"context"
"database/sql"
"fmt"
"log/slog"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/pressly/goose/v3"
)
const (
pgDriverName = "pgx"
migrationsDir = "./migrations"
gooseDriverName = "postgres"
)
func NewConnectionPool(ctx context.Context, connectionString string) (*pgxpool.Pool, error) {
@@ -21,3 +30,29 @@ func NewConnectionPool(ctx context.Context, connectionString string) (*pgxpool.P
return pool, nil
}
func RunMigrations(ctx context.Context, dbURL string) error {
db, err := sql.Open(pgDriverName, dbURL)
if err != nil {
return fmt.Errorf("не удалось открыть соединение для миграций: %w", err)
}
defer db.Close()
goose.SetDialect(gooseDriverName)
statusErr := goose.Status(db, migrationsDir)
if statusErr != nil {
slog.WarnContext(ctx, "Не удалось получить статус миграций", "error", statusErr)
}
current, _ := goose.GetDBVersion(db)
slog.InfoContext(ctx, "Текущая версия БД", "version", current)
slog.DebugContext(ctx, "Запуск миграций Goose...")
if err := goose.Up(db, migrationsDir); err != nil {
return fmt.Errorf("ошибка применения миграций: %w", err)
}
slog.InfoContext(ctx, "Миграции успешно применены или уже актуальны")
return nil
}

View File

19
internal/models/user.go Normal file
View File

@@ -0,0 +1,19 @@
package models
import (
"time"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Slug string `db:"slug" json:"slug"`
PasswordHash string `db:"password_hash" json:"-"`
FullName *string `db:"full_name" json:"full_name,omitempty"`
AvatarURL *string `db:"avatar_url" json:"avatar_url,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt *time.Time `db:"updated_at" json:"updated_at,omitempty"`
LastLoginAt *time.Time `db:"last_login_at" json:"last_login_at,omitempty"`
}

View File

@@ -1,9 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE EXTENSION IF NOT EXISTS pg_tgrm;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP EXTENSION IF EXISTS pg_tgrm;
DROP EXTENSION IF EXISTS pg_trgm;
-- +goose StatementEnd

View File

@@ -7,7 +7,6 @@ CREATE TABLE IF NOT EXISTS users (
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(150),
avatar_url VARCHAR(512),
role VARCHAR(50) DEFAULT 'student',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ,
last_login_at TIMESTAMPTZ

View File

@@ -1,9 +1,22 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
CREATE TABLE IF NOT EXISTS vehicles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plate_number VARCHAR(20) UNIQUE NOT NULL,
brand VARCHAR(50),
model VARCHAR(50),
year INTEGER,
capacity_kg NUMERIC(10,2),
capacity_m3 NUMERIC(10,2),
status VARCHAR(20) DEFAULT 'available',
slug VARCHAR(120) UNIQUE NOT NULL
);
CREATE INDEX idx_vehicles_slug ON vehicles(slug);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
DROP TABLE IF EXISTS vehicles;
-- +goose StatementEnd

View File

@@ -1,9 +1,18 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
CREATE TABLE IF NOT EXISTS drivers(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
vehicle_id UUID REFERENCES vehicles(id) ON DELETE SET NULL,
license_number VARCHAR (50) NOT NULL,
license_expiry DATE NOT NULL,
rating NUMERIC(3,2) DEFAULT 5.00,
slug VARCHAR(120) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'available'
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
DROP TABLE IF EXISTS drivers;
-- +goose StatementEnd

View File

@@ -1,9 +0,0 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd

View File

@@ -0,0 +1,14 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS roles(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(20),
code VARCHAR(20)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS roles;
-- +goose StatementEnd

View File

@@ -0,0 +1,13 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS user_roles(
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS user_roles;
-- +goose StatementEnd

View File

@@ -1,4 +1,4 @@
package models
package storage
import (
"context"