create and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarba
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# DB environment variabels
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=mydb
|
||||
APP_PORT=8080
|
||||
@@ -0,0 +1,33 @@
|
||||
# Билд стадия
|
||||
FROM golang:1.25.1-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем зависимости
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Собираем приложение
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/api
|
||||
|
||||
# Финальная стадия
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# Копируем бинарник из builder стадии
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
# Копируем миграции
|
||||
COPY --from=builder /app/migrations ./migrations
|
||||
|
||||
# Экспозим порт
|
||||
EXPOSE 8080
|
||||
|
||||
# Запускаем приложение
|
||||
CMD ["./main"]
|
||||
@@ -0,0 +1,35 @@
|
||||
.PHONY: build run test clean migrate
|
||||
|
||||
# Переменные
|
||||
APP_NAME=serv_golang_rest_api
|
||||
DOCKER_COMPOSE=docker compose
|
||||
|
||||
# Сборка и запуск
|
||||
build:
|
||||
$(DOCKER_COMPOSE) build
|
||||
|
||||
up:
|
||||
$(DOCKER_COMPOSE) up -d
|
||||
|
||||
down:
|
||||
$(DOCKER_COMPOSE) down
|
||||
|
||||
logs:
|
||||
$(DOCKER_COMPOSE) logs -f api
|
||||
|
||||
# Разработка
|
||||
dev:
|
||||
$(DOCKER_COMPOSE) up db -d
|
||||
go run ./cmd/api
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
# Миграции
|
||||
migrate:
|
||||
$(DOCKER_COMPOSE) exec api ./main migrate
|
||||
|
||||
# Очистка
|
||||
clean:
|
||||
$(DOCKER_COMPOSE) down -v
|
||||
docker system prune -f
|
||||
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"api_tp/internal/config"
|
||||
"api_tp/internal/server"
|
||||
"api_tp/pkg/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Загрузка конфигурации
|
||||
cfg := config.Load()
|
||||
|
||||
// Подключение к БД
|
||||
db, err := database.NewPostgresConnection(cfg)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
|
||||
// Создание и запуск сервера
|
||||
srv := server.New(db)
|
||||
|
||||
log.Printf("Server starting on port %s", cfg.AppPort)
|
||||
if err := srv.Run(cfg.AppPort); err != nil {
|
||||
log.Fatal("Failed to start server:", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
module api_tp
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
@@ -0,0 +1,30 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
type Config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPassword string
|
||||
DBName string
|
||||
AppPort string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
DBHost: getEnv("DB_HOST", "localhost"),
|
||||
DBPort: getEnv("DB_PORT", "5432"),
|
||||
DBUser: getEnv("DB_USER", "postgres"),
|
||||
DBPassword: getEnv("DB_PASSWORD", "postgres"),
|
||||
DBName: getEnv("DB_NAME", "mydb"),
|
||||
AppPort: getEnv("APP_PORT", "8080"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// config/oauth.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/yandex"
|
||||
"golang.org/x/oauth2/vk"
|
||||
)
|
||||
|
||||
|
||||
var (
|
||||
GoogleOAuthConfig = &oauth2.Config{
|
||||
ClientID: "your-google-client-id",
|
||||
ClientSecret: "your-google-client-secret",
|
||||
RedirectURL: "http://localhost:8080/auth/google/callback",
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
YandexOAuthConfig = &oauth2.Config{
|
||||
ClientID: "your-yandex-client-id",
|
||||
ClientSecret: "your-yandex-client-secret",
|
||||
RedirectURL: "http://localhost:8080/auth/yandex/callback",
|
||||
Scopes: []string{"login:email", "login:info", "login:avatar"},
|
||||
Endpoint: yandex.Endpoint,
|
||||
}
|
||||
|
||||
VKOAuthConfig = &oauth2.Config{
|
||||
ClientID: "your-vk-client-id",
|
||||
ClientSecret: "your-vk-client-secret",
|
||||
RedirectURL: "http://localhost:8080/auth/vk/callback",
|
||||
Scopes: []string{"email", "photos"},
|
||||
Endpoint: vk.Endpoint,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
// handlers/auth.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"api_tp/internal/models"
|
||||
"api_tp/internal/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req RegisterRequest
|
||||
if err := utils.DecodeJSON(r, &req); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, существует ли пользователь
|
||||
var existingUser models.User
|
||||
if err := h.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
utils.WriteError(w, http.StatusConflict, "User already exists")
|
||||
return
|
||||
}
|
||||
|
||||
// Хешируем пароль
|
||||
hashedPassword, err := utils.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error creating user")
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем пользователя
|
||||
user := models.User{
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
Name: req.Name,
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&user).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error creating user")
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем JWT токен
|
||||
token, err := utils.GenerateJWT(user.ID, user.Email)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error generating token")
|
||||
return
|
||||
}
|
||||
|
||||
utils.WriteJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"token": token,
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
var req LoginRequest
|
||||
if err := utils.DecodeJSON(r, &req); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
// Ищем пользователя
|
||||
var user models.User
|
||||
if err := h.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials")
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем пароль
|
||||
if !utils.CheckPasswordHash(req.Password, user.Password) {
|
||||
utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials")
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем JWT токен
|
||||
token, err := utils.GenerateJWT(user.ID, user.Email)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error generating token")
|
||||
return
|
||||
}
|
||||
|
||||
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"token": token,
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
)
|
||||
|
||||
func CommonMiddleware() []func(http.Handler) http.Handler {
|
||||
return []func(http.Handler) http.Handler{
|
||||
middleware.Logger,
|
||||
middleware.Recoverer,
|
||||
middleware.Timeout(60 * time.Second),
|
||||
cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"https://*", "http://*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 300,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// handlers/oauth.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"api_tp/internal/config"
|
||||
"api_tp/internal/models"
|
||||
"api_tp/internal/utils"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OAuthHandler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
type GoogleUserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
|
||||
func (h *OAuthHandler) GoogleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
url := config.GoogleOAuthConfig.AuthCodeURL("state")
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
|
||||
token, err := config.GoogleOAuthConfig.Exchange(r.Context(), code)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token")
|
||||
return
|
||||
}
|
||||
|
||||
client := config.GoogleOAuthConfig.Client(r.Context(), token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var userInfo GoogleUserInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info")
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем или находим пользователя
|
||||
user, err := h.findOrCreateOAuthUser("google", userInfo.ID, userInfo.Email, userInfo.Name, token)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error processing user")
|
||||
return
|
||||
}
|
||||
|
||||
jwtToken, err := utils.GenerateJWT(user.ID, user.Email)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error generating token")
|
||||
return
|
||||
}
|
||||
|
||||
// Редирект или возврат токена
|
||||
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"token": jwtToken,
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// Аналогичные методы для Yandex и VK...
|
||||
|
||||
func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name string, token *oauth2.Token) (*models.User, error) {
|
||||
var oauthProvider models.OAuthProvider
|
||||
|
||||
err := h.DB.Where("provider = ? AND provider_id = ?", provider, providerID).
|
||||
Preload("User").
|
||||
First(&oauthProvider).Error
|
||||
|
||||
if err == nil {
|
||||
// Обновляем токены существующей привязки
|
||||
oauthProvider.AccessToken = token.AccessToken
|
||||
oauthProvider.RefreshToken = token.RefreshToken
|
||||
oauthProvider.ExpiresAt = token.Expiry
|
||||
if err := h.DB.Save(&oauthProvider).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := h.DB.First(&user, oauthProvider.UserID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Ищем пользователя по email
|
||||
var user models.User
|
||||
err = h.DB.Where("email = ?", email).First(&user).Error
|
||||
|
||||
if err != nil {
|
||||
// Создаем нового пользователя
|
||||
user = models.User{
|
||||
Email: email,
|
||||
Name: name,
|
||||
Password: utils.GenerateRandomPassword(),
|
||||
}
|
||||
if err := h.DB.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем новую привязку OAuth с токенами
|
||||
oauthProvider = models.OAuthProvider{
|
||||
UserID: user.ID,
|
||||
Provider: provider,
|
||||
ProviderID: providerID,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
ExpiresAt: token.Expiry,
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&oauthProvider).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"api_tp/internal/config"
|
||||
"api_tp/internal/utils"
|
||||
)
|
||||
|
||||
// VKUserInfo представляет данные пользователя от VK
|
||||
type VKUserInfo struct {
|
||||
Response []struct {
|
||||
ID int `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
Photo string `json:"photo_200"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
// VKEmailResponse представляет ответ с email от VK
|
||||
type VKEmailResponse struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// VKLogin initiates VK OAuth flow
|
||||
func (h *OAuthHandler) VKLogin(w http.ResponseWriter, r *http.Request) {
|
||||
url := config.VKOAuthConfig.AuthCodeURL("state")
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
// VKCallback handles VK OAuth callback
|
||||
func (h *OAuthHandler) VKCallback(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
|
||||
token, err := config.VKOAuthConfig.Exchange(r.Context(), code)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// VK не возвращает email в основном токене, нужно получить его отдельно
|
||||
email, err := h.getVKEmail(token.AccessToken)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to get email from VK: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
client := config.VKOAuthConfig.Client(r.Context(), token)
|
||||
|
||||
// Получаем основную информацию о пользователе
|
||||
userInfoURL := fmt.Sprintf("https://api.vk.com/method/users.get?fields=photo_200,email&v=5.131&access_token=%s", token.AccessToken)
|
||||
resp, err := client.Get(userInfoURL)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var vkUserInfo VKUserInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&vkUserInfo); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(vkUserInfo.Response) == 0 {
|
||||
utils.WriteError(w, http.StatusBadRequest, "No user data received from VK")
|
||||
return
|
||||
}
|
||||
|
||||
vkUser := vkUserInfo.Response[0]
|
||||
userID := fmt.Sprintf("%d", vkUser.ID)
|
||||
name := vkUser.FirstName + " " + vkUser.LastName
|
||||
|
||||
// Используем email из отдельного запроса
|
||||
if email == "" && vkUser.Email != "" {
|
||||
email = vkUser.Email
|
||||
}
|
||||
|
||||
// Если email все еще пустой, создаем временный
|
||||
if email == "" {
|
||||
email = fmt.Sprintf("vk_%s@temp.vk", userID)
|
||||
}
|
||||
|
||||
// Создаем или находим пользователя
|
||||
user, err := h.findOrCreateOAuthUser("vk", userID, email, name, token)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error processing user: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
jwtToken, err := utils.GenerateJWT(user.ID, user.Email)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error generating token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.handleOAuthSuccess(w, r, jwtToken, user)
|
||||
}
|
||||
|
||||
// getVKEmail получает email из VK OAuth
|
||||
func (h *OAuthHandler) getVKEmail(accessToken string) (string, error) {
|
||||
// VK возвращает email в ответе на запрос токена, но если его нет,
|
||||
// можно попробовать получить через API
|
||||
emailURL := fmt.Sprintf("https://api.vk.com/method/account.getProfileInfo?v=5.131&access_token=%s", accessToken)
|
||||
|
||||
resp, err := http.Get(emailURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var emailResp struct {
|
||||
Response struct {
|
||||
Email string `json:"email"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&emailResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return emailResp.Response.Email, nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"api_tp/internal/config"
|
||||
"api_tp/internal/models"
|
||||
"api_tp/internal/utils"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type YandexUserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"default_email"`
|
||||
DisplayName string `json:"display_name"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
RealName string `json:"real_name"`
|
||||
IsAvatarEmpty bool `json:"is_avatar_empty"`
|
||||
}
|
||||
|
||||
// YandexLogin initiates Yandex OAuth flow
|
||||
func (h *OAuthHandler) YandexLogin(w http.ResponseWriter, r *http.Request) {
|
||||
url := config.YandexOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
// YandexCallback handles Yandex OAuth callback
|
||||
func (h *OAuthHandler) YandexCallback(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
if state != "state" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Invalid state parameter")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := config.YandexOAuthConfig.Exchange(r.Context(), code)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
client := config.YandexOAuthConfig.Client(r.Context(), token)
|
||||
|
||||
// Получаем информацию о пользователе
|
||||
resp, err := client.Get("https://login.yandex.ru/info?format=json")
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var userInfo YandexUserInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Формируем имя пользователя
|
||||
name := h.getYandexUserName(userInfo)
|
||||
|
||||
// Создаем или находим пользователя
|
||||
user, err := h.findOrCreateOAuthUser("yandex", userInfo.ID, userInfo.Email, name, token)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error processing user: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
jwtToken, err := utils.GenerateJWT(user.ID, user.Email)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "Error generating token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.handleOAuthSuccess(w, r, jwtToken, user)
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) handleOAuthSuccess(w http.ResponseWriter, r *http.Request, jwtToken string, user *models.User) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// getYandexUserName формирует имя пользователя из данных Yandex
|
||||
func (h *OAuthHandler) getYandexUserName(userInfo YandexUserInfo) string {
|
||||
if userInfo.RealName != "" {
|
||||
return userInfo.RealName
|
||||
}
|
||||
if userInfo.DisplayName != "" {
|
||||
return userInfo.DisplayName
|
||||
}
|
||||
if userInfo.FirstName != "" && userInfo.LastName != "" {
|
||||
return userInfo.FirstName + " " + userInfo.LastName
|
||||
}
|
||||
if userInfo.FirstName != "" {
|
||||
return userInfo.FirstName
|
||||
}
|
||||
return userInfo.Login
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"api_tp/internal/models"
|
||||
"api_tp/internal/service"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
return &UserHandler{userService: userService}
|
||||
}
|
||||
|
||||
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.CreateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.CreateUser(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.GetUserByID(uint(id))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetAllUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := h.userService.GetAllUsers()
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(users)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// middleware/auth.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"api_tp/internal/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
utils.WriteError(w, http.StatusUnauthorized, "Authorization header required")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
utils.WriteError(w, http.StatusUnauthorized, "Invalid authorization format")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := utils.ValidateJWT(parts[1])
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusUnauthorized, "Invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем claims в контекст
|
||||
ctx := context.WithValue(r.Context(), "userClaims", claims)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OAuthProvider struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id" gorm:"not null;index:idx_user_provider"` // Уникальный индекс с провайдером
|
||||
Provider string `json:"provider" gorm:"not null;index:idx_user_provider;size:50"` // Ограничение длины
|
||||
ProviderID string `json:"provider_id" gorm:"not null;uniqueIndex:uix_provider_id"` // Уникальный идентификатор
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"` // Добавлено для отслеживания изменений
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Email string `json:"email" gorm:"size:255;uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"size:255;not null"` // Пароль не возвращаем в JSON
|
||||
// OAuth провайдеры
|
||||
OAuthProviders []OAuthProvider `json:"-"`
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Name string `json:"name" validate:"required,min=2,max=100"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Name string `json:"name" validate:"omitempty,min=2,max=100"`
|
||||
Email string `json:"email" validate:"omitempty,email"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_tp/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) Create(user *models.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindAll() ([]models.User, error) {
|
||||
var users []models.User
|
||||
err := r.db.Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) Update(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.User{}, id).Error
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"api_tp/internal/handlers"
|
||||
"api_tp/internal/middleware"
|
||||
"api_tp/internal/repository"
|
||||
"api_tp/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func New(db *gorm.DB) *Server {
|
||||
s := &Server{
|
||||
router: chi.NewRouter(),
|
||||
db: db,
|
||||
}
|
||||
s.configureRouter(db)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) configureRouter(db *gorm.DB) {
|
||||
// Общие middleware
|
||||
for _, middleware := range handlers.CommonMiddleware() {
|
||||
s.router.Use(middleware)
|
||||
}
|
||||
|
||||
// Health check
|
||||
s.router.Get("/health", s.healthCheck)
|
||||
|
||||
// API routes
|
||||
s.router.Route("/v1", func(r chi.Router) {
|
||||
r.Get("/check", s.healthCheck)
|
||||
s.setupUserRoutes(r, db)
|
||||
})
|
||||
|
||||
// Для отладки - выводим все маршруты
|
||||
chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
fmt.Printf("[%s] %s\n", method, route)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) setupUserRoutes(r chi.Router, db *gorm.DB) {
|
||||
userRepo := repository.NewUserRepository(s.db)
|
||||
userService := service.NewUserService(userRepo)
|
||||
userHandler := handlers.NewUserHandler(userService)
|
||||
|
||||
authHandler := &handlers.AuthHandler{DB: db}
|
||||
oauthHandler := &handlers.OAuthHandler{DB: db}
|
||||
|
||||
// Публичные маршруты
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/register", authHandler.Register)
|
||||
r.Post("/login", authHandler.Login)
|
||||
r.Get("/check", s.healthCheck)
|
||||
|
||||
// OAuth routes
|
||||
r.Get("/google", oauthHandler.GoogleLogin)
|
||||
r.Get("/google/callback", oauthHandler.GoogleCallback)
|
||||
r.Get("/yandex", oauthHandler.YandexLogin)
|
||||
r.Get("/yandex/callback", oauthHandler.YandexCallback)
|
||||
r.Get("/vk", oauthHandler.VKLogin)
|
||||
r.Get("/vk/callback", oauthHandler.VKCallback)
|
||||
})
|
||||
|
||||
// Защищенные маршруты
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Use(middleware.AuthMiddleware)
|
||||
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/", userHandler.GetAllUsers)
|
||||
r.Post("/", userHandler.CreateUser)
|
||||
r.Get("/{id}", userHandler.GetUser)
|
||||
r.Get("/check", s.healthCheck)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) healthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем соединение с БД
|
||||
sqlDB, err := s.db.DB()
|
||||
if err != nil {
|
||||
http.Error(w, "Database connection error", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
http.Error(w, "Database ping failed", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC1123),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) Run(port string) error {
|
||||
return http.ListenAndServe(":"+port, s.router)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"api_tp/internal/models"
|
||||
"api_tp/internal/repository"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
userRepo *repository.UserRepository
|
||||
}
|
||||
|
||||
func NewUserService(userRepo *repository.UserRepository) *UserService {
|
||||
return &UserService{userRepo: userRepo}
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUser(req *models.CreateUserRequest) (*models.UserResponse, error) {
|
||||
// Проверяем существует ли пользователь с таким email
|
||||
existingUser, err := s.userRepo.FindByEmail(req.Email)
|
||||
// Проверяем как на nil, так на пустой ID
|
||||
if existingUser != nil && existingUser.ID != 0 {
|
||||
return nil, errors.New("user with this email already exists")
|
||||
}
|
||||
|
||||
// Хешируем пароль
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Password: string(hashedPassword),
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toUserResponse(user), nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetUserByID(id uint) (*models.UserResponse, error) {
|
||||
user, err := s.userRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
return s.toUserResponse(user), nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetAllUsers() ([]models.UserResponse, error) {
|
||||
users, err := s.userRepo.FindAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var responses []models.UserResponse
|
||||
for _, user := range users {
|
||||
responses = append(responses, *s.toUserResponse(&user))
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *UserService) toUserResponse(user *models.User) *models.UserResponse {
|
||||
return &models.UserResponse{
|
||||
ID: user.ID,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// utils/errors.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// APIError представляет ошибку API
|
||||
type APIError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("API Error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// ErrorResponse представляет стандартный ответ с ошибкой
|
||||
type ErrorResponse struct {
|
||||
Error bool `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
// ValidationErrorResponse представляет ответ с ошибками валидации
|
||||
type ValidationErrorResponse struct {
|
||||
Error bool `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
Errors map[string]string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// Predefined errors
|
||||
var (
|
||||
ErrInvalidJSON = &APIError{Code: http.StatusBadRequest, Message: "Invalid JSON"}
|
||||
ErrEmptyRequestBody = &APIError{Code: http.StatusBadRequest, Message: "Request body is empty"}
|
||||
ErrRequestBodyTooLarge = &APIError{Code: http.StatusRequestEntityTooLarge, Message: "Request body too large"}
|
||||
)
|
||||
@@ -0,0 +1,115 @@
|
||||
// utils/json.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DecodeJSON декодирует JSON из тела запроса с валидацией
|
||||
func DecodeJSON(r *http.Request, v interface{}) error {
|
||||
// Ограничиваем размер тела запроса (например, 1MB)
|
||||
maxBytes := int64(1_048_576) // 1MB
|
||||
r.Body = http.MaxBytesReader(nil, r.Body, maxBytes)
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields() // Запрещаем неизвестные поля
|
||||
|
||||
err := decoder.Decode(v)
|
||||
if err != nil {
|
||||
var syntaxError *json.SyntaxError
|
||||
var unmarshalTypeError *json.UnmarshalTypeError
|
||||
var invalidUnmarshalError *json.InvalidUnmarshalError
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
return &APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: "Request body is empty",
|
||||
}
|
||||
case err.Error() == "http: request body too large":
|
||||
return &APIError{
|
||||
Code: http.StatusRequestEntityTooLarge,
|
||||
Message: fmt.Sprintf("Request body must not be larger than %d bytes", maxBytes),
|
||||
}
|
||||
case strings.HasPrefix(err.Error(), "json: unknown field"):
|
||||
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
|
||||
return &APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("Unknown field in JSON: %s", fieldName),
|
||||
}
|
||||
case errors.As(err, &syntaxError):
|
||||
return &APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("Malformed JSON at position %d", syntaxError.Offset),
|
||||
}
|
||||
case errors.As(err, &unmarshalTypeError):
|
||||
return &APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("Invalid value for field '%s'. Expected type %s", unmarshalTypeError.Field, unmarshalTypeError.Type),
|
||||
}
|
||||
case errors.As(err, &invalidUnmarshalError):
|
||||
return &APIError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "Internal server error",
|
||||
}
|
||||
default:
|
||||
return &APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: "Invalid JSON",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, что нет лишних данных после JSON
|
||||
if err = decoder.Decode(&struct{}{}); err != io.EOF {
|
||||
return &APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: "Request body must contain only single JSON object",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteJSON записывает JSON ответ
|
||||
func WriteJSON(w http.ResponseWriter, status int, data interface{}) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetEscapeHTML(true) // Экранируем HTML для безопасности
|
||||
|
||||
return encoder.Encode(data)
|
||||
}
|
||||
|
||||
// WriteError записывает ошибку в формате JSON
|
||||
func WriteError(w http.ResponseWriter, status int, message string) {
|
||||
errorResponse := ErrorResponse{
|
||||
Error: true,
|
||||
Message: message,
|
||||
Code: status,
|
||||
}
|
||||
|
||||
WriteJSON(w, status, errorResponse)
|
||||
}
|
||||
|
||||
// WriteValidationError записывает ошибки валидации
|
||||
func WriteValidationError(w http.ResponseWriter, errors map[string]string) {
|
||||
errorResponse := ValidationErrorResponse{
|
||||
Error: true,
|
||||
Message: "Validation failed",
|
||||
Code: http.StatusBadRequest,
|
||||
Errors: errors,
|
||||
}
|
||||
|
||||
WriteJSON(w, http.StatusBadRequest, errorResponse)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// utils/jwt.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var jwtSecret = []byte("your-secret-key") // вынеси в env variables
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateJWT(userID uint, email string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
func ValidateJWT(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// utils/oauth_utils.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"api_tp/internal/models"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OAuthHandler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// GenerateState generates a random state string for OAuth
|
||||
func GenerateState() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name string, token *oauth2.Token) (*models.User, error) {
|
||||
var oauthProvider models.OAuthProvider
|
||||
|
||||
err := h.DB.Where("provider = ? AND provider_id = ?", provider, providerID).
|
||||
Preload("User").
|
||||
First(&oauthProvider).Error
|
||||
|
||||
if err == nil {
|
||||
// Обновляем токены существующей привязки
|
||||
oauthProvider.AccessToken = token.AccessToken
|
||||
oauthProvider.RefreshToken = token.RefreshToken
|
||||
oauthProvider.ExpiresAt = token.Expiry
|
||||
if err := h.DB.Save(&oauthProvider).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := h.DB.First(&user, oauthProvider.UserID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Ищем пользователя по email
|
||||
var user models.User
|
||||
err = h.DB.Where("email = ?", email).First(&user).Error
|
||||
|
||||
if err != nil {
|
||||
// Создаем нового пользователя
|
||||
user = models.User{
|
||||
Email: email,
|
||||
Name: name,
|
||||
Password: GenerateRandomPassword(),
|
||||
}
|
||||
if err := h.DB.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем новую привязку OAuth с токенами
|
||||
oauthProvider = models.OAuthProvider{
|
||||
UserID: user.ID,
|
||||
Provider: provider,
|
||||
ProviderID: providerID,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
ExpiresAt: token.Expiry,
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&oauthProvider).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// utils/password.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GenerateRandomPassword генерирует случайный пароль для OAuth пользователей
|
||||
func GenerateRandomPassword() string {
|
||||
bytes := make([]byte, 32) // 256 бит
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
// Fallback - используем временный пароль
|
||||
return "temp_oauth_password_123"
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
-- Таблица пользователей
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
-- Индекс для быстрого поиска по email
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
|
||||
-- Индекс для мягкого удаления
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||
@@ -0,0 +1,38 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"api_tp/internal/config"
|
||||
"api_tp/internal/models"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC",
|
||||
cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
// Автомиграция
|
||||
if err := autoMigrate(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Println("Successfully connected to database")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func autoMigrate(db *gorm.DB) error {
|
||||
// автоматические миграции GORM
|
||||
return db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.OAuthProvider{},
|
||||
// другие модели...
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user