T-Mask — это платформа лояльности, интегрированная в экосистему Т-Банка, которая позволяет малому бизнесу создавать и управлять программами лояльности, а клиентам — участвовать в них без необходимости устанавливать отдельные приложения или запоминать коды.
| Layer | Technology |
|---|---|
| Language | Go 1.21+ |
| Web Framework | Fiber v3 |
| ORM | GORM |
| Database | PostgreSQL 14 |
| Auth | JWT (HttpOnly cookies) |
| Password Hashing | bcrypt |
| Migrations | Goose |
| Container | Docker + Docker Compose |
- Clean Architecture - Handler - Service - Repository
- Dependency Injection
- JWT Authentication - jwt храниться в HttpOnly cookies (XSS protection)
- Role-Based Access -
business_owner,client,adminроли
git clone https://github.com/TMaskTeam/backend.git
cd backend
docker-compose up -d --build
sh src/scripts/migrations.sh --up
go mod download
sh src/scripts/migrations.sh --up
go run src/cmd/main.goSERVER_PORT=8080
DATABASE_USER=test_user
DATABASE_PASSWORD=secret
DATABASE_DBNAME=test_db
DATABASE_HOST=localhost
DATABASE_PORT=5432
JWT_SECRET=secrethttp://localhost:8080/api/v1
Для защищённых эндпоинтов используется HttpOnly cookie, которая устанавливается автоматически после логина. Браузер сам отправляет её при каждом запросе.
Cookie: token=<JWT_TOKEN>
{
"error": "inn is already used"
}Request:
{
"first_name": "Иван",
"last_name": "Петров",
"middle_name": "Иванович",
"inn": "123456789012",
"phone_number": "+79001234567",
"email": "ivan@example.com",
"birthday": "1990-01-15",
"password": "MyP@ssw0rd123"
}Response (201 Created):
{
"owner_id": 1,
"first_name": "Иван",
"last_name": "Петров",
"inn": "123456789012",
"phone_number": "+79001234567",
"email": "ivan@example.com",
"birthday": "1990-01-15T00:00:00Z"
}Validation Rules:
| Field | Rules |
|---|---|
first_name |
Required, min 2 chars |
last_name |
Required, min 2 chars |
inn |
Required, 10 or 12 digits |
phone_number |
Required, valid format |
email |
Required, valid email |
password |
Required, min 8 chars |
Request:
{
"login": "ivan@example.com",
"password": "MyP@ssw0rd123"
}Response (200 OK):
{
"owner_id": 1,
"first_name": "Иван",
"last_name": "Петров",
"inn": "123456789012",
"phone_number": "+79001234567",
"email": "ivan@example.com",
"birthday": "1990-01-15T00:00:00Z"
}JWT token устанавливается как HttpOnly cookie автоматически.
Request:
{
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"email": "anna@example.com",
"birthday": "1995-05-20",
"password": "ClientPass123"
}Response (201 Created):
{
"client_id": 1,
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"email": "anna@example.com",
"birthday": "1995-05-20T00:00:00Z"
}Request:
{
"login": "anna@example.com",
"password": "ClientPass123"
}Response (200 OK):
{
"client_id": 1,
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"email": "anna@example.com",
"birthday": "1995-05-20T00:00:00Z"
}Request: (требует авторизации)
Cookie: token=<JWT_TOKEN>
Response: 204 No Content
Request: (требует авторизации)
Cookie: token=<JWT_TOKEN>
Response для владельца бизнеса (200 OK):
{
"role": "business_owner",
"owner_id": 1,
"first_name": "Иван",
"last_name": "Петров",
"inn": "123456789012",
"phone_number": "+79001234567",
"email": "ivan@example.com",
"birthday": "1990-01-15T00:00:00Z"
}Response для клиента (200 OK):
{
"role": "client",
"client_id": 1,
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"email": "anna@example.com",
"birthday": "1995-05-20T00:00:00Z"
}Request: (требует авторизации)
{
"first_name": "Петр",
"last_name": "Сидоров",
"phone_number": "+79001112233",
"email": "petr@example.com",
"password": "NewPassword123"
}Все поля опциональны
Response (200 OK): Тот же формат, что и GET /me с обновлёнными данными
Request: (требует авторизации)
{
"name": "Кофе Хаус",
"address": "ул. Тверская, 15"
}Response (201 Created):
{
"business_id": 1,
"owner_id": 1,
"name": "Кофе Хаус",
"address": "ул. Тверская, 15",
"created_at": "2025-05-17T10:30:00Z",
"updated_at": "2025-05-17T10:30:00Z"
}Request: (требует авторизации)
Response (200 OK):
{
"businesses": [
{
"business_id": 1,
"name": "Кофе Хаус",
"address": "ул. Тверская, 15"
},
{
"business_id": 2,
"name": "Пиццерия",
"address": "ул. Арбат, 10"
}
]
}Request: (требует авторизации)
DELETE /api/v1/businesses/1
Response: 204 No Content
Request: (требует авторизации — владелец бизнеса)
{
"program_name": "Кофейная карта",
"token_name": "кофеины"
}Response (201 Created):
{
"program_id": 1,
"business_id": 1,
"program_name": "Кофейная карта",
"token_name": "кофеины"
}Request:
Response (200 OK):
{
"programs": [
{
"program_id": 1,
"business_id": 1,
"program_name": "Кофейная карта",
"token_name": "кофеины"
}
]
}Request:
Response (200 OK):
{
"programs": [
{
"program_id": 1,
"business_id": 1,
"business_name": "Кофе Хаус",
"program_name": "Кофейная карта",
"token_name": "кофеины"
}
]
}Request: (требует авторизации — роль client)
Response (200 OK):
{
"message": "successfully joined the program"
}Вот все недостающие эндпоинты в том же формате. Добавь их в конец секции API.
Request: (требует авторизации — владелец бизнеса)
GET /api/v1/businesses/1
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"business_id": 1,
"owner_id": 1,
"name": "Кофе Хаус",
"address": "ул. Тверская, 15",
"created_at": "2025-05-17T10:30:00Z",
"updated_at": "2025-05-17T10:30:00Z"
}Request: (требует авторизации — владелец бизнеса)
{
"name": "Кофе Хаус Updated",
"address": "ул. Тверская, 20"
}Все поля опциональны
Response (200 OK):
{
"business_id": 1,
"owner_id": 1,
"name": "Кофе Хаус Updated",
"address": "ул. Тверская, 20",
"created_at": "2025-05-17T10:30:00Z",
"updated_at": "2025-05-17T12:00:00Z"
}Request:
GET /api/v1/programs/1
Response (200 OK):
{
"program_id": 1,
"business_id": 1,
"business_name": "Кофе Хаус",
"program_name": "Кофейная карта",
"token_name": "кофеины",
"created_at": "2025-05-17T10:30:00Z",
"updated_at": "2025-05-17T10:30:00Z"
}Request: (требует авторизации — владелец бизнеса)
{
"program_name": "Кофейная карта Platinum",
"token_name": "платина"
}Все поля опциональны
Response (200 OK):
{
"program_id": 1,
"business_id": 1,
"business_name": "Кофе Хаус",
"program_name": "Кофейная карта Platinum",
"token_name": "платина",
"created_at": "2025-05-17T10:30:00Z",
"updated_at": "2025-05-17T12:00:00Z"
}Request: (требует авторизации — владелец бизнеса)
DELETE /api/v1/programs/1
Response: 204 No Content
Request: (требует авторизации — владелец бизнеса)
{
"visit_tokens": 1,
"percentage_purchase_tokens": 5,
"register_tokens": 10,
"birthday_tokens": 50,
"friend_invite_tokens": 20,
"minimum_receipt_limit": 500
}Response (201 Created):
{
"program_info_id": 1,
"program_id": 1,
"visit_tokens": 1,
"percentage_purchase_tokens": 5,
"register_tokens": 10,
"birthday_tokens": 50,
"friend_invite_tokens": 20,
"minimum_receipt_limit": 500,
"created_at": "2025-05-17T10:30:00Z",
"updated_at": "2025-05-17T10:30:00Z"
}Request:
Response (200 OK):
{
"program_info_id": 1,
"program_id": 1,
"visit_tokens": 1,
"percentage_purchase_tokens": 5,
"register_tokens": 10,
"birthday_tokens": 50,
"friend_invite_tokens": 20,
"minimum_receipt_limit": 500,
"created_at": "2025-05-17T10:30:00Z",
"updated_at": "2025-05-17T10:30:00Z"
}Request: (требует авторизации — владелец бизнеса)
{
"visit_tokens": 2,
"percentage_purchase_tokens": 7,
"minimum_receipt_limit": 300
}Все поля опциональны
Response (200 OK): Тот же формат, что и GET, с обновлёнными данными
Request: (требует авторизации — роль client)
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"programs": [
{
"program_id": 1,
"business_name": "Кофе Хаус",
"program_name": "Кофейная карта",
"token_name": "кофеины",
"balance": 125,
"joined_at": "2025-05-17T10:30:00Z"
}
]
}Request: (требует авторизации — роль client)
GET /api/v1/client/programs/1
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"program_id": 1,
"business_name": "Кофе Хаус",
"program_name": "Кофейная карта",
"token_name": "кофеины",
"balance": 125,
"total_earned": 450,
"total_spent": 325,
"total_visits": 23,
"current_discount": 1.2,
"joined_at": "2025-05-17T10:30:00Z",
"last_visit_at": "2025-05-17T15:30:00Z"
}Request: (требует авторизации — владелец бизнеса)
GET /api/v1/programs/1/participants
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"participants": [
{
"client_id": 1,
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"email": "anna@example.com",
"balance": 125,
"total_visits": 23,
"joined_at": "2025-05-17T10:30:00Z",
"last_visit_at": "2025-05-17T15:30:00Z"
}
],
"total": 1
}Request: (требует авторизации — владелец бизнеса)
GET /api/v1/programs/1/participants/1
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"client_id": 1,
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"email": "anna@example.com",
"balance": 125,
"total_earned": 450,
"total_spent": 325,
"total_visits": 23,
"current_discount": 1.2,
"joined_at": "2025-05-17T10:30:00Z",
"last_visit_at": "2025-05-17T15:30:00Z"
}Request: (требует авторизации — webhook или админ)
{
"client_id": 1,
"program_id": 1,
"ruble_amount": 500,
"description": "Оплата в кафе"
}Либо ruble_amount, либо tokens
Response (200 OK):
{
"transaction_id": 1,
"client_id": 1,
"program_id": 1,
"amount": 25,
"new_balance": 150,
"description": "Оплата 500₽, начислено 25 кофеинов"
}Request: (требует авторизации — клиент или кассир)
{
"client_id": 1,
"program_id": 1,
"tokens": 50,
"description": "Скидка 50₽ на чек"
}Response (200 OK):
{
"transaction_id": 2,
"client_id": 1,
"program_id": 1,
"amount": -50,
"new_balance": 100,
"description": "Списано 50 кофеинов"
}Request: (требует авторизации — клиент)
GET /api/v1/client/programs/1/balance
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"program_id": 1,
"program_name": "Кофейная карта",
"token_name": "кофеины",
"balance": 125,
"total_earned": 450,
"total_spent": 325
}Request: (требует авторизации — владелец бизнеса)
GET /api/v1/programs/1/participants
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"participants": [
{
"client_id": 1,
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"balance": 125,
"total_visits": 23,
"joined_at": "2025-05-17T10:30:00Z"
}
],
"total": 1
}Request: (требует авторизации — владелец бизнеса)
GET /api/v1/programs/1/participants/1
Cookie: token=<JWT_TOKEN>
Response (200 OK):
{
"client_id": 1,
"first_name": "Анна",
"last_name": "Сидорова",
"phone_number": "+79009999999",
"balance": 125,
"total_earned": 450,
"total_spent": 325,
"total_visits": 23,
"joined_at": "2025-05-17T10:30:00Z",
"last_visit_at": "2025-05-17T15:30:00Z"
}Request: (требует авторизации — webhook)
{
"client_id": 1,
"program_id": 1,
"ruble_amount": 500,
"description": "Оплата в кафе"
}Response (200 OK):
{
"transaction_id": 1,
"client_id": 1,
"program_id": 1,
"amount": 25,
"new_balance": 150,
"description": "Оплата 500₽, начислено 25 кофеинов"
}Request: (требует авторизации — клиент)
{
"client_id": 1,
"program_id": 1,
"tokens": 50,
"description": "Скидка 50₽ на чек"
}Response (200 OK):
{
"transaction_id": 2,
"client_id": 1,
"program_id": 1,
"amount": -50,
"new_balance": 100,
"description": "Списано 50 кофеинов"
}| Таблица | Миграция |
|---|---|
business_owner |
20260515071000_create_business_owner_table.sql |
client |
20260516145306_create_client_table.sql |
business |
20260515132902_business_table.sql |
bonus_program |
20260515132947_bonus_program_table.sql |
bonus_program_info |
20260515133020_bonus_program_info_table.sql |
client_bonus_program |
20260516145347_create_client_bonus_table.sql |
| Аспект | Реализация |
|---|---|
| Пароли | bcrypt хэширование |
| Токены | JWT в HttpOnly cookies (XSS защита) |
| Срок жизни токена | 24 часа |
| Доступ по ролям | Проверка role в JWT |
| CORS | Настраиваемые allowed origins |
Лицензия: MIT
2025 T-Mask Team.