to much to describe, everything in PR

This commit is contained in:
2026-03-24 20:25:18 +05:00
parent 36f2551c60
commit 87df56f41e
13 changed files with 1138 additions and 290 deletions

View File

@@ -22,3 +22,9 @@ accessTokenTTL = "24h"
[jwt]
issuer = "logiflow-server"
audience = "logiflow-client"
[pricing]
baseFee = 500.0
perKm = 25.0
perKg = 3.0
perM3 = 150.0

View File

@@ -10,9 +10,6 @@ servers:
- url: http://localhost:3001
description: Local development
security:
- BearerAuth: []
components:
securitySchemes:
BearerAuth:
@@ -53,7 +50,7 @@ components:
RegisterRequest:
type: object
required: [email, password, fullName, role]
required: [email, password, fullName]
properties:
email:
type: string
@@ -63,9 +60,6 @@ components:
minLength: 8
fullName:
type: string
role:
type: string
enum: [admin, manager, driver, client]
LoginRequest:
type: object
@@ -323,11 +317,16 @@ components:
ManagerCreate:
type: object
required: [userId]
required: [email, password, fullName]
properties:
userId:
email:
type: string
format: email
password:
type: string
minLength: 8
fullName:
type: string
format: uuid
warehouseId:
type: string
format: uuid
@@ -363,11 +362,16 @@ components:
DriverCreate:
type: object
required: [userId, licenseNumber, licenseExpiry]
required: [email, password, fullName, licenseNumber, licenseExpiry]
properties:
userId:
email:
type: string
format: email
password:
type: string
minLength: 8
fullName:
type: string
format: uuid
vehicleId:
type: string
format: uuid
@@ -488,6 +492,84 @@ components:
format: uuid
nullable: true
# ─── Notification ─────────────────────────────────────────────────────────
Notification:
type: object
properties:
id:
type: string
format: uuid
userId:
type: string
format: uuid
title:
type: string
body:
type: string
nullable: true
isRead:
type: boolean
createdAt:
type: string
format: date-time
# ─── Report ───────────────────────────────────────────────────────────────
DriverStatusUpdate:
type: object
required: [status]
properties:
status:
type: string
enum: [available, on_route, off_duty]
# ─── Dashboard ────────────────────────────────────────────────────────────
DashboardDriverStat:
type: object
properties:
id:
type: string
format: uuid
fullName:
type: string
status:
type: string
enum: [available, on_route, off_duty]
rating:
type: number
completedOrders:
type: integer
DashboardReport:
type: object
properties:
revenue:
type: object
properties:
total:
type: number
thisMonth:
type: number
orders:
type: object
properties:
total:
type: integer
delivered:
type: integer
inTransit:
type: integer
pending:
type: integer
cancelled:
type: integer
drivers:
type: array
items:
$ref: "#/components/schemas/DashboardDriverStat"
# ─── Route ────────────────────────────────────────────────────────────────
Coordinate:
@@ -555,7 +637,8 @@ paths:
/auth/register:
post:
operationId: authRegister
summary: Регистрация нового пользователя
summary: Регистрация клиента
description: Публичный эндпоинт — создаёт аккаунт с ролью client. Для создания менеджеров и водителей используйте POST /managers и POST /drivers (только admin).
tags: [Auth]
security: []
requestBody:
@@ -706,6 +789,22 @@ paths:
"401":
description: Не авторизован
/me/trips:
get:
operationId: getMyTrips
summary: Поездки текущего водителя
tags: [Me]
description: Доступно только для роли driver. Возвращает маршруты, назначенные на текущего водителя.
responses:
"200":
description: Список маршрутов
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"403":
description: Доступно только водителям
# ─── Vehicles ───────────────────────────────────────────────────────────────
/vehicles:
@@ -899,7 +998,8 @@ paths:
post:
operationId: createManager
summary: Назначить менеджера
summary: Создать менеджера
description: Только для роли admin. Создаёт пользователя с ролью manager и профиль менеджера в одной транзакции.
tags: [Managers]
requestBody:
required: true
@@ -954,6 +1054,13 @@ paths:
operationId: listDrivers
summary: Список водителей
tags: [Drivers]
parameters:
- name: status
in: query
required: false
schema:
type: string
enum: [available, on_route, off_duty]
responses:
"200":
description: Список водителей
@@ -964,7 +1071,8 @@ paths:
post:
operationId: createDriver
summary: Зарегистрировать водителя
summary: Создать водителя
description: Только для роли admin. Создаёт пользователя с ролью driver и профиль водителя в одной транзакции.
tags: [Drivers]
requestBody:
required: true
@@ -1030,6 +1138,27 @@ paths:
"200":
description: Водитель удалён
/drivers/me/status:
patch:
operationId: updateMyDriverStatus
summary: Водитель меняет свой статус
tags: [Drivers]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DriverStatusUpdate"
responses:
"200":
description: Статус обновлён
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"403":
description: Доступно только водителям
# ─── Orders ─────────────────────────────────────────────────────────────────
/orders:
@@ -1040,6 +1169,19 @@ paths:
description: >
Клиент видит только свои заявки, менеджер — заявки своего склада,
водитель — назначенные на него, администратор — все.
parameters:
- name: status
in: query
required: false
schema:
type: string
enum: [pending, assigned, in_transit, delivered, cancelled]
- name: driverId
in: query
required: false
schema:
type: string
format: uuid
responses:
"200":
description: Список заявок
@@ -1191,3 +1333,110 @@ paths:
description: Switching Protocols — WebSocket установлен
"404":
description: Маршрут не найден
# ─── Notifications ───────────────────────────────────────────────────────────
/notifications:
get:
operationId: listNotifications
summary: Список уведомлений текущего пользователя
tags: [Notifications]
parameters:
- name: unreadOnly
in: query
required: false
schema:
type: boolean
responses:
"200":
description: Список уведомлений
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"401":
description: Не авторизован
/notifications/{id}/read:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
patch:
operationId: markNotificationRead
summary: Отметить уведомление как прочитанное
tags: [Notifications]
responses:
"200":
description: Отмечено
"404":
description: Не найдено
# ─── Reports ─────────────────────────────────────────────────────────────────
/reports/orders:
get:
operationId: getOrdersReport
summary: Отчёт по заявкам
tags: [Reports]
description: Доступно для ролей admin и manager. Возвращает агрегированные данные по завершённым заявкам за период.
parameters:
- name: from
in: query
required: false
schema:
type: string
format: date
- name: to
in: query
required: false
schema:
type: string
format: date
- name: status
in: query
required: false
schema:
type: string
enum: [pending, assigned, in_transit, delivered, cancelled]
- name: driverId
in: query
required: false
schema:
type: string
format: uuid
- name: warehouseId
in: query
required: false
schema:
type: string
format: uuid
responses:
"200":
description: Отчёт сформирован
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"403":
description: Нет доступа
/reports/dashboard:
get:
operationId: getDashboard
summary: Дашборд менеджмента
description: Доступно для ролей admin и manager. Возвращает агрегированную статистику по выручке, заявкам и водителям.
tags: [Reports]
responses:
"200":
description: Данные дашборда
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
"403":
description: Нет доступа

File diff suppressed because it is too large Load Diff

View File

@@ -57,6 +57,13 @@ type Config struct {
Issuer string `koanf:"issuer"`
Audience string `koanf:"audience"`
} `koanf:"jwt"`
Pricing struct {
BaseFee float64 `koanf:"baseFee"`
PerKm float64 `koanf:"perKm"`
PerKg float64 `koanf:"perKg"`
PerM3 float64 `koanf:"perM3"`
} `koanf:"pricing"`
}
func NewConfig(ctx context.Context, configPath string) (*Config, error) {
@@ -152,6 +159,18 @@ func (c *Config) setDefaults() {
if c.Redis.Addr == "" {
c.Redis.Addr = "localhost:6379"
}
if c.Pricing.BaseFee == 0 {
c.Pricing.BaseFee = 500.0
}
if c.Pricing.PerKm == 0 {
c.Pricing.PerKm = 25.0
}
if c.Pricing.PerKg == 0 {
c.Pricing.PerKg = 3.0
}
if c.Pricing.PerM3 == 0 {
c.Pricing.PerM3 = 150.0
}
}
func (c *Config) parseDurations() error {

View File

@@ -1,13 +1,195 @@
package handler
import "net/http"
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"time"
func (s *Server) ListDrivers(w http.ResponseWriter, r *http.Request) {}
"github.com/anxi0uz/logiflow/internal/api"
"github.com/anxi0uz/logiflow/internal/models"
storage "github.com/anxi0uz/logiflow/pkg"
"github.com/google/uuid"
"github.com/gosimple/slug"
"github.com/huandu/go-sqlbuilder"
"golang.org/x/crypto/bcrypt"
)
func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {}
func (s *Server) ListDrivers(w http.ResponseWriter, r *http.Request, params api.ListDriversParams) {}
func (s *Server) GetDriver(w http.ResponseWriter, r *http.Request, slug string) {}
func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims := ctx.Value("user").(*Claims)
if claims.Role != "admin" {
slog.WarnContext(ctx, "unusual try from not allowed role", slog.String("Role", claims.Role), slog.String("id", claims.ID.String()))
s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError)
return
}
var req api.DriverCreate
func (s *Server) UpdateDriver(w http.ResponseWriter, r *http.Request, slug string) {}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
slog.ErrorContext(ctx, "Invalid request body", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
return
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
slog.ErrorContext(ctx, "error while generating passwordhash", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
now := time.Now()
userid := uuid.New()
user := models.User{
ID: userid,
Slug: s.GenerateUserSlug(req.FullName, userid),
CreatedAt: now,
UpdatedAt: now,
Role: "driver",
Email: string(req.Email),
PasswordHash: string(passwordHash),
FullName: req.FullName,
}
func (s *Server) DeleteDriver(w http.ResponseWriter, r *http.Request, slug string) {}
tx, err := s.DB.Begin(ctx)
if err != nil {
slog.ErrorContext(ctx, "Unable to open transaction", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
defer tx.Rollback(ctx)
if err := storage.Create(ctx, "users", user, tx); err != nil {
slog.ErrorContext(ctx, "Unable to create user", slog.String("error", err.Error()), slog.Any("user", user))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
id := uuid.New()
driver := models.Driver{
ID: id,
UserID: userid,
VehicleID: req.VehicleId,
LicenseNumber: req.LicenseNumber,
LicenseExpiry: req.LicenseExpiry.Time,
Rating: 0,
Slug: slug.Make(req.FullName + " " + req.LicenseNumber),
}
if err := storage.Create(ctx, "drivers", driver, tx); err != nil {
slog.ErrorContext(ctx, "Unable to create driver", slog.String("error", err.Error()), slog.Any("driver", driver))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
if err := tx.Commit(ctx); err != nil {
slog.ErrorContext(ctx, "Error while commiting transaction", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusCreated, driver, "driver")
}
func (s *Server) UpdateMyDriverStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
if !ok {
slog.ErrorContext(ctx, "Unable to convert claims", slog.Any("claims", ctx.Value("user")))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
var req api.DriverStatusUpdate
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
return
}
if err := storage.Update(ctx, "drivers", models.Driver{Status: string(req.Status)}, s.DB, func(ub *sqlbuilder.UpdateBuilder) {
ub.Where(ub.Equal("user_id", claims.ID))
}); err != nil {
if errors.Is(err, storage.ErrNotFound) {
slog.ErrorContext(ctx, "No driver with that user id not found", slog.String("user id", claims.ID.String()))
s.JSON(w, r, http.StatusNotFound, "driver not found", RespNotFound)
return
}
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, "status updated", RespSuccess)
}
func (s *Server) GetDriver(w http.ResponseWriter, r *http.Request, slug string) {
ctx := r.Context()
driver, err := storage.GetOne[models.Driver](ctx, s.DB, "drivers", func(sb *sqlbuilder.SelectBuilder) {
sb.Where(sb.Equal("slug", slug))
})
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
slog.ErrorContext(ctx, "No driver found with that slug", slog.String("slug", slug))
s.JSON(w, r, http.StatusNotFound, MsgNotFound, RespNotFound)
return
}
slog.ErrorContext(ctx, "Error while finding driver with that slug", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, driver, RespSuccess)
}
func (s *Server) UpdateDriver(w http.ResponseWriter, r *http.Request, slug string) {
ctx := r.Context()
var req api.DriverUpdate
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
slog.ErrorContext(ctx, "Invalid request body", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusBadRequest, MsgInternalError, RespError)
return
}
driver := models.Driver{
LicenseNumber: *req.LicenseNumber,
LicenseExpiry: req.LicenseExpiry.Time,
Status: string(*req.Status),
VehicleID: req.VehicleId,
}
if err := storage.Update(ctx, "drivers", driver, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
sb.Where(sb.Equal("slug", slug))
}); err != nil {
if errors.Is(err, storage.ErrNotFound) {
slog.ErrorContext(ctx, "no drivers found with that slug", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusNotFound, MsgNotFound, RespNotFound)
return
}
slog.ErrorContext(ctx, "Error while updating driver with that slug", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, slug, RespSuccess)
}
func (s *Server) DeleteDriver(w http.ResponseWriter, r *http.Request, slug string) {
ctx := r.Context()
err := storage.Delete[models.Driver](ctx, "drivers", s.DB, func(sb *sqlbuilder.DeleteBuilder) {
sb.Where(sb.Equal("slug", slug))
})
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
s.JSON(w, r, http.StatusNotFound, "No driver with that slug found", RespNotFound)
return
}
slog.ErrorContext(ctx, "unable to delete vehicle with that slug", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, slug, "deleted")
}

View File

@@ -0,0 +1,14 @@
package handler
import (
"net/http"
"github.com/anxi0uz/logiflow/internal/api"
openapi_types "github.com/oapi-codegen/runtime/types"
)
func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, params api.ListNotificationsParams) {
}
func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
}

View File

@@ -3,10 +3,11 @@ package handler
import (
"net/http"
"github.com/anxi0uz/logiflow/internal/api"
openapi_types "github.com/oapi-codegen/runtime/types"
)
func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request) {}
func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.ListOrdersParams) {}
func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {}
@@ -15,3 +16,8 @@ func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_typ
func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {}
func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {}
func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params api.GetOrdersReportParams) {
}
func (s *Server) GetDashboard(w http.ResponseWriter, r *http.Request) {}

View File

@@ -26,6 +26,19 @@ type ctxKey string
const (
requestIDKey ctxKey = "X-Request-ID"
tokenKey ctxKey = "Authorization"
// response messages
MsgInternalError = "Internal server error"
MsgInvalidBody = "Invalid request body"
MsgNotFound = "Not found"
MsgUnauthorized = "Unauthorized"
MsgMissingToken = "Missing token"
MsgForbidden = "Forbidden"
// response types
RespError = "error"
RespSuccess = "success"
RespNotFound = "not found"
)
type responseOptions struct {
@@ -112,7 +125,7 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
tokenStr := r.Header.Get("Authorization")
if tokenStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "missing token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgMissingToken, RespError)
return
}
@@ -121,7 +134,7 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
claims, err := s.validateAccessToken(r.Context(), tokenStr)
if err != nil {
slog.WarnContext(r.Context(), "Токен не прошел валидацию", slog.String("Token", tokenStr), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusUnauthorized, "токен не прошёл валидацию", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgUnauthorized, RespError)
return
}

View File

@@ -33,7 +33,7 @@ func (s *Server) AuthLogin(w http.ResponseWriter, r *http.Request) {
var req api.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.JSON(w, r, http.StatusBadRequest, "Ошибка при получении данных", "error")
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
return
}
@@ -65,32 +65,32 @@ func (s *Server) AuthLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("refresh_token")
if err != nil {
s.JSON(w, r, http.StatusUnauthorized, "Missing refresh token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgMissingToken, RespError)
return
}
refreshStr := cookie.Value
if refreshStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "Empty refresh token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgUnauthorized, RespError)
return
}
refreshKey := "refresh_token:" + refreshStr
if err := s.Redis.Del(ctx, refreshKey).Err(); err != nil {
slog.ErrorContext(ctx, "Error while removing refresh token from redis", slog.String("token", refreshStr), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusUnauthorized, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
tokenStr := r.Header.Get("Authorization")
if tokenStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "missing token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgMissingToken, RespError)
return
}
tokenKey := "access_token:" + tokenStr
if err := s.Redis.Del(ctx, tokenKey).Err(); err != nil {
slog.ErrorContext(ctx, "Error while removing access token from redis", slog.String("token", tokenStr), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusUnauthorized, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
}
@@ -99,17 +99,17 @@ func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("refresh_token")
if err != nil {
s.JSON(w, r, http.StatusUnauthorized, "Missing refresh token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgMissingToken, RespError)
return
}
refreshStr := cookie.Value
if refreshStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "Empty refresh token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgUnauthorized, RespError)
return
}
key := "refresh_token:" + refreshStr
if _, err := s.Redis.Get(ctx, key).Result(); err == redis.Nil {
s.JSON(w, r, http.StatusUnauthorized, "No active refresh token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgUnauthorized, RespError)
return
}
@@ -119,13 +119,13 @@ func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
if !ok {
slog.ErrorContext(ctx, "Error parsing claims", slog.Any("claims", claims))
s.JSON(w, r, http.StatusInternalServerError, nil, "internal server error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
userID := claims.ID
if userID == uuid.Nil {
s.JSON(w, r, http.StatusUnauthorized, "Missing user id in token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgUnauthorized, RespError)
return
}
@@ -134,7 +134,7 @@ func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
slog.ErrorContext(ctx, "user with that id not found", slog.Any("id", userID.String()), "error", err.Error())
s.JSON(w, r, http.StatusUnauthorized, "Invalid user id in token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgUnauthorized, RespError)
return
}
s.issueTokens(w, r, user)
@@ -146,13 +146,13 @@ func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {
var req api.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.JSON(w, r, http.StatusBadRequest, "invalid request body", "error")
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
return
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
s.JSON(w, r, http.StatusInternalServerError, "error while hashing password", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
@@ -165,7 +165,7 @@ func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {
Slug: s.GenerateUserSlug(req.FullName, uuid),
CreatedAt: now,
UpdatedAt: now,
Role: string(req.Role),
Role: "client",
Email: string(req.Email),
PasswordHash: string(passwordHash),
FullName: req.FullName,
@@ -173,7 +173,7 @@ func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {
if err := storage.Create(ctx, "users", user, s.DB); err != nil {
slog.ErrorContext(ctx, "Error while creating user", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.issueTokens(w, r, &user)
@@ -185,14 +185,14 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
claims, ok := claimsValue.(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
s.JSON(w, r, http.StatusInternalServerError, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
userID := claims.ID
tx, err := s.DB.Begin(ctx)
if err != nil {
slog.ErrorContext(ctx, "Error while begining transaction", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
defer tx.Rollback(ctx)
@@ -202,7 +202,7 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
slog.ErrorContext(ctx, "Error while deleting user", slog.String("error", err.Error()), slog.String("id", userID.String()))
s.JSON(w, r, http.StatusInternalServerError, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
@@ -210,29 +210,29 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
tokenKey := "access_token:" + jwt
if err := s.Redis.Del(ctx, tokenKey).Err(); err != nil {
slog.ErrorContext(ctx, "Error while deleting access token from redis", slog.String("token", jwt))
s.JSON(w, r, http.StatusInternalServerError, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
cookie, err := r.Cookie("refresh_token")
if err != nil {
s.JSON(w, r, http.StatusUnauthorized, "Missing refresh token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgMissingToken, RespError)
return
}
refreshStr := cookie.Value
if refreshStr == "" {
s.JSON(w, r, http.StatusUnauthorized, "Empty refresh token", "error")
s.JSON(w, r, http.StatusUnauthorized, MsgUnauthorized, RespError)
return
}
key := "refresh_token:" + refreshStr
if err := s.Redis.Del(ctx, key).Err(); err != nil {
slog.ErrorContext(ctx, "Error while deleting refresh token from redis", slog.String("token", refreshStr))
s.JSON(w, r, http.StatusInternalServerError, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
if err := tx.Commit(ctx); err != nil {
slog.ErrorContext(ctx, "Error while committing transaction", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
@@ -246,7 +246,7 @@ func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
claims, ok := claimsValue.(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
s.JSON(w, r, http.StatusInternalServerError, "Internal server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
@@ -258,13 +258,15 @@ func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
slog.ErrorContext(ctx, "No user was found with that id",
slog.String("id", userID.String()),
slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "invalid user id", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, user, "ok")
}
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {}
func (s *Server) GetMyTrips(w http.ResponseWriter, r *http.Request) {}
func (s *Server) issueTokens(w http.ResponseWriter, r *http.Request, user *models.User) {
access, err := s.generateAccessToken(user, s.Config.RedisAccessTokenDur())
if err != nil {
@@ -274,7 +276,7 @@ func (s *Server) issueTokens(w http.ResponseWriter, r *http.Request, user *model
refresh, err := s.generateRefreshToken()
if err != nil {
slog.ErrorContext(r.Context(), "generate refresh token failed", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Failure during generating tokens", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
@@ -283,14 +285,14 @@ func (s *Server) issueTokens(w http.ResponseWriter, r *http.Request, user *model
err = s.Redis.Set(r.Context(), key, "valid", s.Config.RedisAccessTokenDur()).Err()
if err != nil {
slog.ErrorContext(r.Context(), "Failed to set access token in redis", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
err = s.Redis.Set(r.Context(), refreshkey, "valid", s.Config.RedisRefreshTokenDur()).Err()
if err != nil {
slog.ErrorContext(r.Context(), "Failed to set refresh token in redis", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, "Server error", "error")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}

View File

@@ -1,13 +1,155 @@
package handler
import "net/http"
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
func (s *Server) ListVehicles(w http.ResponseWriter, r *http.Request) {}
"github.com/anxi0uz/logiflow/internal/api"
"github.com/anxi0uz/logiflow/internal/models"
storage "github.com/anxi0uz/logiflow/pkg"
"github.com/google/uuid"
"github.com/gosimple/slug"
"github.com/huandu/go-sqlbuilder"
)
func (s *Server) CreateVehicle(w http.ResponseWriter, r *http.Request) {}
func (s *Server) ListVehicles(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
func (s *Server) GetVehicle(w http.ResponseWriter, r *http.Request, slug string) {}
vehicles, err := storage.GetAll[models.Vehicle](ctx, "vehicles", s.DB)
if err != nil {
slog.ErrorContext(ctx, "Unable to get all vehicles", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, vehicles, RespSuccess)
}
func (s *Server) UpdateVehicle(w http.ResponseWriter, r *http.Request, slug string) {}
func (s *Server) CreateVehicle(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
func (s *Server) DeleteVehicle(w http.ResponseWriter, r *http.Request, slug string) {}
var req api.VehicleCreate
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
slog.ErrorContext(ctx, "Invalid request body", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
return
}
id := uuid.New()
vehicle := models.Vehicle{
ID: id,
PlateNumber: req.PlateNumber,
Slug: slug.Make(req.PlateNumber),
}
if req.Brand != nil {
vehicle.Brand = *req.Brand
}
if req.Model != nil {
vehicle.Model = *req.Model
}
if req.Year != nil {
vehicle.Year = *req.Year
}
if req.CapacityKg != nil {
vehicle.CapacityKg = float64(*req.CapacityKg)
}
if req.CapacityM3 != nil {
vehicle.CapacityM3 = float64(*req.CapacityM3)
}
if err := storage.Create(ctx, "vehicles", vehicle, s.DB); err != nil {
slog.ErrorContext(ctx, "Unable to create vehicle", slog.Any("vehicle", vehicle))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusCreated, vehicle, "vehicle")
}
func (s *Server) GetVehicle(w http.ResponseWriter, r *http.Request, slug string) {
ctx := r.Context()
vehicle, err := storage.GetOne[models.Vehicle](ctx, s.DB, "vehicles", func(sb *sqlbuilder.SelectBuilder) {
sb.Where(sb.Equal("slug", slug))
})
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
s.JSON(w, r, http.StatusNotFound, "vehicle not found with that slug", RespNotFound)
return
}
slog.ErrorContext(ctx, "no vehicle found in db with that slug", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, vehicle, "vehicle")
}
func (s *Server) UpdateVehicle(w http.ResponseWriter, r *http.Request, slug string) {
ctx := r.Context()
var req api.VehicleUpdate
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
slog.ErrorContext(ctx, "Invalid request body", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
return
}
vehicle, err := storage.GetOne[models.Vehicle](ctx, s.DB, "vehicles", func(sb *sqlbuilder.SelectBuilder) {
sb.Where(sb.Equal("slug", slug))
})
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
s.JSON(w, r, http.StatusNotFound, "vehicle not found with that slug", RespNotFound)
return
}
slog.ErrorContext(ctx, "no vehicle found in db with that slug", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
if req.Status != nil {
vehicle.Status = string(*req.Status)
}
if req.Brand != nil {
vehicle.Brand = *req.Brand
}
if req.Model != nil {
vehicle.Model = *req.Model
}
if req.Year != nil {
vehicle.Year = *req.Year
}
if req.CapacityKg != nil {
vehicle.CapacityKg = float64(*req.CapacityKg)
}
if req.CapacityM3 != nil {
vehicle.CapacityM3 = float64(*req.CapacityM3)
}
if err := storage.Update(ctx, "vehicles", *vehicle, s.DB, func(ub *sqlbuilder.UpdateBuilder) {
ub.Where(ub.Equal("slug", slug))
}); err != nil {
slog.ErrorContext(ctx, "failed to update vehicle", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, vehicle, "vehicle")
}
func (s *Server) DeleteVehicle(w http.ResponseWriter, r *http.Request, slug string) {
ctx := r.Context()
err := storage.Delete[models.Vehicle](ctx, "vehicles", s.DB, func(sb *sqlbuilder.DeleteBuilder) {
sb.Where(sb.Equal("slug", slug))
})
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
s.JSON(w, r, http.StatusNotFound, "no vehicle with that slug found", RespNotFound)
return
}
slog.ErrorContext(ctx, "unable to delete vehicle with that slug", slog.String("slug", slug), slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.JSON(w, r, http.StatusOK, slug, "deleted")
}

View File

@@ -0,0 +1,16 @@
package models
import (
"time"
"github.com/google/uuid"
)
type Notification struct {
ID uuid.UUID `db:"id"`
UserID uuid.UUID `db:"user_id"`
Title string `db:"title"`
Body *string `db:"body"`
IsRead bool `db:"is_read"`
CreatedAt time.Time `db:"created_at"`
}

View File

@@ -1,9 +1,19 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
body TEXT,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_notifications_user_id ON notifications(user_id);
CREATE INDEX idx_notifications_user_unread ON notifications(user_id) WHERE is_read = FALSE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
DROP TABLE IF EXISTS notifications;
-- +goose StatementEnd

View File

@@ -23,8 +23,6 @@ func GetAll[T any](ctx context.Context, table string, db Querier, opts ...func(*
sb.From(table)
sb.OrderByDesc("created_at")
for _, opt := range opts {
opt(sb)
}