Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,31 @@ curl -X POST http://localhost:8080/api/users \

#### Аутентификация пользователя (получение JWT-токена)
```bash
curl -X POST http://localhost:8080/api/login \
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "password123"}'
-d '{"username": "testuser", "password": "password123"}'
```

#### Выход пользователя (отзыв refresh токена)
```bash
curl -X POST http://localhost:8080/api/auth/logout \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {jwt_token}" \
-d '{"refresh_token": "{refresh_token}"}'
```

#### Обновление токенов
```bash
curl -X POST http://localhost:8080/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "{refresh_token}"}'
```

#### Проверка валидности токена
```bash
curl -X POST http://localhost:8080/api/auth/validate \
-H "Content-Type: application/json" \
-d '{"token": "{jwt_token}"}'
```

После успешной аутентификации вы получите JWT-токен. При использовании токена в других запросах не включайте фигурные скобки `{}` - они используются только для обозначения плейсхолдера в примерах.
Expand Down Expand Up @@ -199,3 +221,65 @@ curl -X DELETE http://localhost:8080/api/notes/{note_id} \
- `make test` - запуск тестов
- `make build` - сборка приложения
- `make help` - список всех целей

## Переменные окружения

Приложение поддерживает настройку через переменные окружения. Ниже приведены доступные переменные:

### Общие настройки
- `ENV` - окружение (development, production, staging) (по умолчанию: development)
- `LOG_LEVEL` - уровень логирования (по умолчанию: info)

### Сервер
- `SERVER_PORT` - порт для HTTP сервера (по умолчанию: :8080)
- `GRPC_PORT` - порт для gRPC сервера (по умолчанию: :50051)
- `SERVER_READ_TIMEOUT` - таймаут чтения запроса в секундах (по умолчанию: 15)
- `SERVER_WRITE_TIMEOUT` - таймаут записи ответа в секундах (по умолчанию: 15)

### Аутентификация
- `AUTH_ENABLE_HTTPS` - включить HTTPS (по умолчанию: false)

### PostgreSQL
- `POSTGRES_HOST` - хост PostgreSQL (по умолчанию: localhost)
- `POSTGRES_PORT` - порт PostgreSQL (по умолчанию: 5432)
- `POSTGRES_NAME` - имя базы данных (по умолчанию: go_notes)
- `POSTGRES_USER` - имя пользователя базы данных (по умолчанию: postgres)
- `POSTGRES_PASSWORD` - пароль базы данных
- `POSTGRES_SSL_MODE` - режим SSL для PostgreSQL (по умолчанию: disable)
- `POSTGRES_POOL_SIZE` - размер пула подключений (по умолчанию: 10)
- `POSTGRES_PARAMETERS` - дополнительные параметры подключения

### Redis
- `REDIS_HOST` - хост Redis (по умолчанию: localhost)
- `REDIS_PORT` - порт Redis (по умолчанию: 6379)
- `REDIS_PASSWORD` - пароль Redis
- `REDIS_DB` - номер базы данных Redis (по умолчанию: 0)
- `REDIS_POOL_SIZE` - размер пула подключений (по умолчанию: 10)
- `REDIS_URL` - альтернативный способ указания подключения

### JWT
- `JWT_SECRET_KEY` - секретный ключ для подписи JWT токенов (по умолчанию: my_secret_key)
- `JWT_ALGORITHM` - алгоритм подписи токена (по умолчанию: HS256)
- `BCRYPT_COST` - стоимость хеширования паролей (по умолчанию: 10)
- `ACCESS_TOKEN_TTL` - время жизни access токена (по умолчанию: 15m)
- `REFRESH_TOKEN_TTL` - время жизни refresh токена (по умолчанию: 168h)

### Refresh токены
- `REFRESH_SECRET_KEY` - секретный ключ для подписи Refresh токенов (по умолчанию: refresh_secret_key)
- `REFRESH_REVOCATION_ENABLED` - включено ли отслеживание отозванных токенов (по умолчанию: true)
- `REFRESH_REVOCATION_STORE_TYPE` - тип хранилища для отозванных токенов (по умолчанию: memory)

### Репозиторий
- `REPO_TYPE` - тип репозитория (json, ram, postgres) (по умолчанию: json)
- `REPO_PATH` - путь к файлу/директории для хранения данных (по умолчанию: ./data)

### Безопасность
- `PASSWORD_MIN_LENGTH` - минимальная длина пароля (по умолчанию: 8)
- `MAX_LOGIN_ATTEMPTS` - максимальное количество попыток входа (по умолчанию: 5)
- `LOGIN_BLOCK_TIME` - время блокировки после неудачных попыток (по умолчанию: 30m)
- `TOKEN_CLEANUP_INTERVAL` - интервал очистки токенов (по умолчанию: 1h)
- `BCRYPT_COST_SEC` - стоимость хеширования паролей (по умолчанию: 10)

### Завершение работы
- `SHUTDOWN_TIMEOUT` - таймаут завершения работы (по умолчанию: 5s)
- `SHUTDOWN_WAIT` - время ожидания перед завершением (по умолчанию: 3s)
74 changes: 74 additions & 0 deletions api/proto/auth/auth.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
syntax = "proto3";

package auth;

option go_package = "github.com/rd2w/go-notes/pkg/proto/auth";

// AuthService предоставляет методы для аутентификации и авторизации
service AuthService {
// Login - метод для аутентификации пользователя и получения токенов
rpc Login(LoginRequest) returns (LoginResponse);

// Logout - метод для выхода пользователя и отзыва токена
rpc Logout(LogoutRequest) returns (LogoutResponse);

// Refresh - метод для обновления токена
rpc Refresh(RefreshRequest) returns (RefreshResponse);

// ValidateToken - метод для проверки валидности токена
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
}

// LoginRequest - запрос на аутентификацию
message LoginRequest {
string username = 1;
string password = 2;
}

// LoginResponse - ответ на аутентификацию
message LoginResponse {
string access_token = 1;
string refresh_token = 2;
int64 access_token_expires_at = 3; // Время истечения access токена в Unix timestamp
int64 refresh_token_expires_at = 4; // Время истечения refresh токена в Unix timestamp
string token_type = 5; // Тип токена (обычно "Bearer")
}

// LogoutRequest - запрос на выход
message LogoutRequest {
string access_token = 1;
string refresh_token = 2;
}

// LogoutResponse - ответ на выход
message LogoutResponse {
bool success = 1;
string message = 2;
}

// RefreshRequest - запрос на обновление токена
message RefreshRequest {
string refresh_token = 1;
}

// RefreshResponse - ответ на обновление токена
message RefreshResponse {
string access_token = 1;
string refresh_token = 2;
int64 access_token_expires_at = 3; // Время истечения нового access токена в Unix timestamp
int64 refresh_token_expires_at = 4; // Время истечения нового refresh токена в Unix timestamp (может быть обновлено)
string token_type = 5; // Тип токена (обычно "Bearer")
}

// ValidateTokenRequest - запрос на проверку токена
message ValidateTokenRequest {
string token = 1;
}

// ValidateTokenResponse - ответ на проверку токена
message ValidateTokenResponse {
bool valid = 1;
string username = 2;
int64 expires_at = 3; // Время истечения токена в Unix timestamp
string error_message = 4; // Сообщение об ошибке, если токен недействителен
}
136 changes: 92 additions & 44 deletions cmd/grpc-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,37 @@ import (
"log"
"time"

note "github.com/rd2w/go-notes/pkg/proto/note"
user "github.com/rd2w/go-notes/pkg/proto/user"
"github.com/rd2w/go-notes/internal/config"
authpb "github.com/rd2w/go-notes/pkg/proto/auth"
"github.com/rd2w/go-notes/pkg/proto/note"
"github.com/rd2w/go-notes/pkg/proto/user"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)

const (
address = "localhost:50051"
)
// Добавляем вспомогательную функцию для создания контекста с токеном
func createAuthContext(ctx context.Context, token string) context.Context {
if token != "" {
return metadata.NewOutgoingContext(ctx, metadata.Pairs("authorization", "Bearer "+token))
}
return ctx
}

func main() {
// Загружаем конфигурацию
cfg, err := config.LoadConfig("config/config_dev.toml")
if err != nil {
log.Printf("Предупреждение: не удалось загрузить конфигурацию из config_dev.toml: %v", err)
log.Println("Используем конфигурацию по умолчанию")
cfg = config.NewDefaultConfigWithValues()
}

// Формируем адрес gRPC сервера
grpcAddress := "localhost" + cfg.Server.GRPCPort

// Устанавливаем соединение с gRPC сервером
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
conn, err := grpc.NewClient(grpcAddress, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Не удалось подключиться к gRPC серверу: %v", err)
}
Expand All @@ -28,14 +46,47 @@ func main() {
}
}()

client := note.NewNotesServiceClient(conn)
// Создаем клиентов для разных сервисов
noteClient := note.NewNotesServiceClient(conn)
userClient := user.NewUserServiceClient(conn)
authClient := authpb.NewAuthServiceClient(conn)

// Сначала регистрируем и логиним пользователя для получения токенов
fmt.Println("=== Регистрация и аутентификация пользователя ===")

// Создание пользователя
fmt.Println("\n1. Создание пользователя:")
createUserResp, err := userClient.CreateUser(context.Background(), &user.CreateUserRequest{
Username: "testuser",
Email: "test@example.com",
Password: "password123",
})
if err != nil {
log.Printf("Ошибка при создании пользователя: %v", err)
} else {
fmt.Printf("Создан пользователь: ID=%s, Имя=%s, Email=%s\n", createUserResp.User.Id, createUserResp.User.Username, createUserResp.User.Email)
}

// Логинимся для получения токенов
fmt.Println("\n2. Аутентификация пользователя:")
loginResp, err := authClient.Login(context.Background(), &authpb.LoginRequest{
Username: "testuser",
Password: "password123",
})
if err != nil {
log.Fatalf("Ошибка при аутентификации: %v", err)
}
fmt.Printf("Успешная аутентификация. Access токен: %s\n", loginResp.AccessToken)

// Создаем контекст с токеном для аутентифицированных запросов
authCtx := createAuthContext(context.Background(), loginResp.AccessToken)

// Тестирование операций с заметками
fmt.Println("=== Тестирование операций с заметками ===")
// Тестирование операций с заметками с аутентификацией
fmt.Println("\n=== Тестирование операций с заметками (с аутентификацией) ===")

// Создание заметки
fmt.Println("\n1. Создание заметки:")
createNoteResp, err := client.CreateNote(context.Background(), &note.CreateNoteRequest{
fmt.Println("\n3. Создание заметки:")
createNoteResp, err := noteClient.CreateNote(authCtx, &note.CreateNoteRequest{
Title: "Тестовая заметка",
Content: "Это содержимое тестовой заметки",
})
Expand All @@ -46,8 +97,8 @@ func main() {
}

// Получение списка заметок
fmt.Println("\n2. Получение списка заметок:")
listNotesResp, err := client.ListNotes(context.Background(), &note.Empty{})
fmt.Println("\n4. Получение списка заметок:")
listNotesResp, err := noteClient.ListNotes(authCtx, &note.Empty{})
if err != nil {
log.Printf("Ошибка при получении списка заметок: %v", err)
} else {
Expand All @@ -60,17 +111,17 @@ func main() {
// Если есть хотя бы одна заметка, получаем её по ID и обновляем
if len(listNotesResp.Notes) > 0 {
firstNote := listNotesResp.Notes[0]
fmt.Printf("\n3. Получение заметки по ID (%s):\n", firstNote.Id)
getNoteResp, err := client.GetNote(context.Background(), &note.GetRequest{Id: firstNote.Id})
fmt.Printf("\n5. Получение заметки по ID (%s):\n", firstNote.Id)
getNoteResp, err := noteClient.GetNote(authCtx, &note.GetRequest{Id: firstNote.Id})
if err != nil {
log.Printf("Ошибка при получении заметки: %v", err)
} else {
fmt.Printf("Получена заметка: ID=%s, Заголовок=%s, Содержимое=%s\n",
getNoteResp.Note.Id, getNoteResp.Note.Title, getNoteResp.Note.Content)
}

fmt.Printf("\n4. Обновление заметки (%s):\n", firstNote.Id)
updateNoteResp, err := client.UpdateNote(context.Background(), &note.UpdateNoteRequest{
fmt.Printf("\n6. Обновление заметки (%s):\n", firstNote.Id)
updateNoteResp, err := noteClient.UpdateNote(authCtx, &note.UpdateNoteRequest{
Id: firstNote.Id,
Title: "Обновленная тестовая заметка",
Content: "Это обновленное содержимое тестовой заметки",
Expand All @@ -82,26 +133,12 @@ func main() {
}
}

// Тестирование операций с пользователями
fmt.Println("\n=== Тестирование операций с пользователями ===")

// Создание пользователя
fmt.Println("\n1. Создание пользователя:")
userClient := user.NewUserServiceClient(conn)
createUserResp, err := userClient.CreateUser(context.Background(), &user.CreateUserRequest{
Username: "testuser",
Email: "test@example.com",
Password: "password123",
})
if err != nil {
log.Printf("Ошибка при создании пользователя: %v", err)
} else {
fmt.Printf("Создан пользователь: ID=%s, Имя=%s, Email=%s\n", createUserResp.User.Id, createUserResp.User.Username, createUserResp.User.Email)
}
// Тестирование операций с пользователями с аутентификацией
fmt.Println("\n=== Тестирование операций с пользователями (с аутентификацией) ===")

// Получение списка пользователей
fmt.Println("\n2. Получение списка пользователей:")
listUsersResp, err := userClient.ListUsers(context.Background(), &user.Empty{})
fmt.Println("\n7. Получение списка пользователей:")
listUsersResp, err := userClient.ListUsers(authCtx, &user.Empty{})
if err != nil {
log.Printf("Ошибка при получении списка пользователей: %v", err)
} else {
Expand All @@ -114,17 +151,17 @@ func main() {
// Если есть хотя бы один пользователь, получаем его по ID и обновляем
if len(listUsersResp.Users) > 0 {
firstUser := listUsersResp.Users[0]
fmt.Printf("\n3. Получение пользователя по ID (%s):\n", firstUser.Id)
getUserResp, err := userClient.GetUser(context.Background(), &user.GetRequest{Id: firstUser.Id})
fmt.Printf("\n8. Получение пользователя по ID (%s):\n", firstUser.Id)
getUserResp, err := userClient.GetUser(authCtx, &user.GetRequest{Id: firstUser.Id})
if err != nil {
log.Printf("Ошибка при получении пользователя: %v", err)
} else {
fmt.Printf("Получен пользователь: ID=%s, Имя=%s, Email=%s\n",
getUserResp.User.Id, getUserResp.User.Username, getUserResp.User.Email)
}

fmt.Printf("\n4. Обновление пользователя (%s):\n", firstUser.Id)
updateUserResp, err := userClient.UpdateUser(context.Background(), &user.UpdateUserRequest{
fmt.Printf("\n9. Обновление пользователя (%s):\n", firstUser.Id)
updateUserResp, err := userClient.UpdateUser(authCtx, &user.UpdateUserRequest{
Id: firstUser.Id,
Username: "updateduser",
Email: "updated@example.com",
Expand All @@ -141,8 +178,8 @@ func main() {

// Удаление последней созданной заметки
if createNoteResp != nil {
fmt.Printf("\n5. Удаление заметки (%s):\n", createNoteResp.Note.Id)
deleteNoteResp, err := client.DeleteNote(context.Background(), &note.GetRequest{Id: createNoteResp.Note.Id})
fmt.Printf("\n10. Удаление заметки (%s):\n", createNoteResp.Note.Id)
deleteNoteResp, err := noteClient.DeleteNote(authCtx, &note.GetRequest{Id: createNoteResp.Note.Id})
if err != nil {
log.Printf("Ошибка при удалении заметки: %v", err)
} else {
Expand All @@ -152,14 +189,25 @@ func main() {

// Удаление последнего созданного пользователя
if createUserResp != nil {
fmt.Printf("\n6. Удаление пользователя (%s):\n", createUserResp.User.Id)
deleteUserResp, err := userClient.DeleteUser(context.Background(), &user.GetRequest{Id: createUserResp.User.Id})
fmt.Printf("\n11. Удаление пользователя (%s):\n", createUserResp.User.Id)
deleteUserResp, err := userClient.DeleteUser(authCtx, &user.GetRequest{Id: createUserResp.User.Id})
if err != nil {
log.Printf("Ошибка при удалении пользователя: %v", err)
} else {
fmt.Printf("Результат удаления: %t, Сообщение: %s\n", deleteUserResp.Success, deleteUserResp.Message)
}
}

fmt.Println("\nТестирование gRPC клиента завершено.")
// Логаут
fmt.Println("\n12. Выход из системы:")
logoutResp, err := authClient.Logout(authCtx, &authpb.LogoutRequest{
RefreshToken: loginResp.RefreshToken,
})
if err != nil {
log.Printf("Ошибка при выходе: %v", err)
} else {
fmt.Printf("Результат выхода: %t, Сообщение: %s\n", logoutResp.Success, logoutResp.Message)
}

fmt.Println("\nТестирование gRPC клиента с аутентификацией завершено.")
}
Loading