Created base BE. Auth + /current endpoint
This commit is contained in:
60
.gitignore
vendored
60
.gitignore
vendored
@@ -1,24 +1,40 @@
|
||||
# Compiled class file
|
||||
*.class
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
# Log file
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Environment files ###
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
### Logs ###
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
# ====== Build stage ======
|
||||
FROM maven:3.9.9-eclipse-temurin-17 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pom.xml .
|
||||
RUN mvn -q -e -B dependency:go-offline
|
||||
|
||||
COPY src ./src
|
||||
|
||||
RUN mvn -q -e -B clean package -DskipTests
|
||||
|
||||
# ====== Runtime stage ======
|
||||
FROM eclipse-temurin:17-jre
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy fat jar from build stage
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
# Expose port (for local/docker-compose/documentation)
|
||||
EXPOSE 8080
|
||||
|
||||
ENV JAVA_OPTS=""
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||
|
||||
27
Dockerfile.inferno
Normal file
27
Dockerfile.inferno
Normal file
@@ -0,0 +1,27 @@
|
||||
# ====== Build stage ======
|
||||
FROM maven:3.9.9-eclipse-temurin-17 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pom.xml .
|
||||
RUN mvn -q -e -B dependency:go-offline
|
||||
|
||||
COPY src ./src
|
||||
|
||||
RUN mvn -q -e -B clean package -DskipTests
|
||||
|
||||
# ====== Runtime stage ======
|
||||
FROM eclipse-temurin:17-jre
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy fat jar from build stage
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
# Expose port (for internal communication with nginx)
|
||||
EXPOSE 8080
|
||||
|
||||
ENV JAVA_OPTS=""
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||
|
||||
542
README.md
Normal file
542
README.md
Normal file
@@ -0,0 +1,542 @@
|
||||
# Honey Backend
|
||||
|
||||
Spring Boot backend application for Honey project.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Java 17**
|
||||
- **Spring Boot 3.2.0**
|
||||
- **MySQL 8.0** (using INT/BIGINT only, no floating point numbers)
|
||||
- **Flyway** (database migrations)
|
||||
- **Docker** (containerization)
|
||||
- **Maven** (build tool)
|
||||
|
||||
## Local Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 17 JDK
|
||||
- Maven 3.9+
|
||||
- Docker and Docker Compose (for local MySQL)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
2. **Start MySQL using Docker Compose**:
|
||||
```bash
|
||||
docker-compose up -d db
|
||||
```
|
||||
|
||||
3. **Create `.env` file** (for local development):
|
||||
```env
|
||||
DB_NAME=honey_db
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=password
|
||||
DB_ROOT_PASSWORD=password
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
4. **Run the application**:
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
Or build and run with Docker:
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
Flyway automatically runs migrations on startup. Migrations are located in `src/main/resources/db/migration/`.
|
||||
|
||||
## Deployment Guides
|
||||
|
||||
### Railway Deployment (Staging Environment)
|
||||
|
||||
Railway is the primary deployment platform for staging. It provides built-in logging and easy environment variable management.
|
||||
|
||||
#### Step 1: Create Railway Project
|
||||
|
||||
1. Go to [Railway](https://railway.app) and sign in
|
||||
2. Click **"New Project"**
|
||||
3. Select **"Empty Project"**
|
||||
|
||||
#### Step 2: Create MySQL Database Service
|
||||
|
||||
1. In your Railway project, click **"+ New"** → **"Database"** → **"Add MySQL"**
|
||||
2. Railway will automatically create a MySQL database
|
||||
3. Note the connection details (you'll need them for the backend service)
|
||||
|
||||
#### Step 3: Create Backend Service
|
||||
|
||||
1. In your Railway project, click **"+ New"** → **"GitHub Repo"** (or **"Empty Service"**)
|
||||
2. If using GitHub:
|
||||
- Connect your GitHub account
|
||||
- Select the `honey-be` repository
|
||||
- Railway will automatically detect it's a Java/Maven project
|
||||
3. If using Empty Service:
|
||||
- Click **"Empty Service"**
|
||||
- Connect to your repository or upload files
|
||||
|
||||
#### Step 4: Configure Environment Variables
|
||||
|
||||
In your backend service settings, go to **"Variables"** and add:
|
||||
|
||||
```env
|
||||
SPRING_DATASOURCE_URL=${MYSQL_URL}
|
||||
SPRING_DATASOURCE_USERNAME=${MYSQLUSER}
|
||||
SPRING_DATASOURCE_PASSWORD=${MYSQLPASSWORD}
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
|
||||
FRONTEND_URL=https://your-frontend-url.railway.app
|
||||
PORT=8080
|
||||
```
|
||||
|
||||
**Note**: Railway automatically provides `MYSQL_URL`, `MYSQLUSER`, and `MYSQLPASSWORD` when you add a MySQL database. You can reference them using `${MYSQL_URL}` syntax.
|
||||
|
||||
#### Step 5: Link Database to Backend
|
||||
|
||||
1. In your backend service, go to **"Settings"** → **"Connect"**
|
||||
2. Find your MySQL database service
|
||||
3. Click **"Connect"** - Railway will automatically add the MySQL connection variables
|
||||
|
||||
#### Step 6: Configure Health Check
|
||||
|
||||
1. In your backend service, go to **"Settings"** → **"Healthcheck"**
|
||||
2. Set **Healthcheck Path** to:
|
||||
```
|
||||
/actuator/health/readiness
|
||||
```
|
||||
3. Railway will poll this endpoint and wait for HTTP 200 before marking deployment as active
|
||||
|
||||
#### Step 7: Deploy
|
||||
|
||||
1. Railway will automatically deploy when you push to the connected branch
|
||||
2. Or manually trigger deployment from the Railway dashboard
|
||||
3. Check the **"Deployments"** tab to monitor the deployment
|
||||
|
||||
#### Step 8: Get Backend URL
|
||||
|
||||
1. In your backend service, go to **"Settings"** → **"Networking"**
|
||||
2. Click **"Generate Domain"** to get a public URL
|
||||
3. Or use the default Railway domain
|
||||
4. Copy the URL (e.g., `https://honey-be-production.up.railway.app`)
|
||||
|
||||
#### Step 9: Create Frontend Service (Optional - if deploying frontend to Railway)
|
||||
|
||||
1. In your Railway project, click **"+ New"** → **"GitHub Repo"**
|
||||
2. Select your `honey-fe` repository
|
||||
3. Railway will detect it's a Node.js project
|
||||
4. Add environment variable:
|
||||
```env
|
||||
VITE_API_BASE_URL=https://your-backend-url.railway.app
|
||||
```
|
||||
5. Railway will automatically build and deploy
|
||||
|
||||
#### Step 10: Create Volume (Optional - for persistent data)
|
||||
|
||||
If you need persistent storage:
|
||||
|
||||
1. In your Railway project, click **"+ New"** → **"Volume"**
|
||||
2. Name it (e.g., `honey-data`)
|
||||
3. Mount it to your service if needed
|
||||
|
||||
### Inferno Deployment (Production Environment)
|
||||
|
||||
Inferno Solution provides the production environment. It requires manual server setup and uses Docker Compose with nginx.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Access to Inferno Solution server (via SSH)
|
||||
- Docker and Docker Compose installed on the server
|
||||
- JDK 17 installed on the server (for building, though Docker handles runtime)
|
||||
- Domain name configured (optional, for HTTPS)
|
||||
|
||||
#### Step 1: Prepare Server
|
||||
|
||||
1. **SSH into your Inferno server**:
|
||||
```bash
|
||||
ssh user@your-server-ip
|
||||
```
|
||||
|
||||
2. **Install Docker** (if not installed):
|
||||
```bash
|
||||
# For Ubuntu/Debian
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
3. **Install Docker Compose** (if not installed):
|
||||
```bash
|
||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
4. **Install JDK 17** (for building):
|
||||
```bash
|
||||
# For Ubuntu/Debian
|
||||
sudo apt update
|
||||
sudo apt install openjdk-17-jdk -y
|
||||
```
|
||||
|
||||
5. **Create project directory**:
|
||||
```bash
|
||||
mkdir -p /opt/honey
|
||||
cd /opt/honey
|
||||
```
|
||||
|
||||
#### Step 2: Clone Repository
|
||||
|
||||
```bash
|
||||
cd /opt/honey
|
||||
git clone https://github.com/your-username/honey-be.git
|
||||
cd honey-be
|
||||
```
|
||||
|
||||
#### Step 3: Create Secret Configuration File
|
||||
|
||||
Create a tmpfs mount for secrets (more secure than .env files):
|
||||
|
||||
```bash
|
||||
# Create tmpfs mount point
|
||||
sudo mkdir -p /run/secrets
|
||||
sudo chmod 700 /run/secrets
|
||||
|
||||
# Create secret file
|
||||
sudo nano /run/secrets/honey-config.properties
|
||||
```
|
||||
|
||||
Add the following content (replace with your actual values):
|
||||
|
||||
```properties
|
||||
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/honey_db
|
||||
SPRING_DATASOURCE_USERNAME=honey_user
|
||||
SPRING_DATASOURCE_PASSWORD=your_secure_mysql_password
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
|
||||
FRONTEND_URL=https://your-frontend-domain.com
|
||||
MYSQL_PASSWORD=your_secure_mysql_password
|
||||
MYSQL_ROOT_PASSWORD=your_secure_mysql_root_password
|
||||
```
|
||||
|
||||
**Important**:
|
||||
- Use strong, unique passwords
|
||||
- Keep this file secure (it's in tmpfs, so it's in-memory only)
|
||||
- The file will be lost on reboot - you'll need to recreate it or use a startup script
|
||||
|
||||
#### Step 4: Configure Docker Compose for Inferno
|
||||
|
||||
The `docker-compose.inferno.yml` file is already configured. Make sure it's present in your repository.
|
||||
|
||||
#### Step 5: Build and Start Services
|
||||
|
||||
```bash
|
||||
cd /opt/honey/honey-be
|
||||
|
||||
# Build and start all services
|
||||
docker-compose -f docker-compose.inferno.yml up -d --build
|
||||
```
|
||||
|
||||
This will:
|
||||
- Build the backend application
|
||||
- Start MySQL database
|
||||
- Start backend service
|
||||
- Start nginx reverse proxy
|
||||
|
||||
#### Step 6: Configure Nginx (if using custom domain)
|
||||
|
||||
1. **Edit nginx configuration**:
|
||||
```bash
|
||||
nano nginx/conf.d/honey.conf
|
||||
```
|
||||
|
||||
2. **Update server_name** (if using HTTPS):
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
# ... rest of config
|
||||
}
|
||||
```
|
||||
|
||||
3. **Reload nginx**:
|
||||
```bash
|
||||
docker-compose -f docker-compose.inferno.yml restart nginx
|
||||
```
|
||||
|
||||
#### Step 7: Set Up SSL (Optional - Recommended for Production)
|
||||
|
||||
1. **Install Certbot** (Let's Encrypt):
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx -y
|
||||
```
|
||||
|
||||
2. **Obtain SSL certificate**:
|
||||
```bash
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
3. **Update nginx config** to use HTTPS (uncomment HTTPS server block in `nginx/conf.d/honey.conf`)
|
||||
|
||||
4. **Reload nginx**:
|
||||
```bash
|
||||
docker-compose -f docker-compose.inferno.yml restart nginx
|
||||
```
|
||||
|
||||
#### Step 8: Configure Firewall
|
||||
|
||||
```bash
|
||||
# Allow HTTP and HTTPS
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
|
||||
# Allow SSH (if not already allowed)
|
||||
sudo ufw allow 22/tcp
|
||||
|
||||
# Enable firewall
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
#### Step 9: Set Up Auto-Start on Boot
|
||||
|
||||
Create a systemd service to ensure services start on boot:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/honey.service
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Honey Application
|
||||
Requires=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/honey/honey-be
|
||||
ExecStart=/usr/local/bin/docker-compose -f docker-compose.inferno.yml up -d
|
||||
ExecStop=/usr/local/bin/docker-compose -f docker-compose.inferno.yml down
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable honey.service
|
||||
sudo systemctl start honey.service
|
||||
```
|
||||
|
||||
#### Step 10: Set Up Grafana Integration (Production Logging)
|
||||
|
||||
1. **Install Grafana and Loki** (on a separate server or same server):
|
||||
```bash
|
||||
# Follow Grafana/Loki installation guide
|
||||
# https://grafana.com/docs/loki/latest/installation/
|
||||
```
|
||||
|
||||
2. **Configure Promtail** to collect logs from Docker containers:
|
||||
```yaml
|
||||
# promtail-config.yml
|
||||
server:
|
||||
http_listen_port: 9080
|
||||
grpc_listen_port: 0
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
|
||||
clients:
|
||||
- url: http://loki:3100/loki/api/v1/push
|
||||
|
||||
scrape_configs:
|
||||
- job_name: honey-backend
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
refresh_interval: 5s
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_name]
|
||||
regex: honey-backend
|
||||
action: keep
|
||||
```
|
||||
|
||||
3. **Update docker-compose.inferno.yml** to add logging driver:
|
||||
```yaml
|
||||
app:
|
||||
# ... existing config
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
```
|
||||
|
||||
4. **Configure Grafana datasource** to connect to Loki
|
||||
|
||||
#### Step 11: Monitor and Maintain
|
||||
|
||||
**Check service status**:
|
||||
```bash
|
||||
docker-compose -f docker-compose.inferno.yml ps
|
||||
```
|
||||
|
||||
**View logs**:
|
||||
```bash
|
||||
# All services
|
||||
docker-compose -f docker-compose.inferno.yml logs -f
|
||||
|
||||
# Specific service
|
||||
docker-compose -f docker-compose.inferno.yml logs -f app
|
||||
```
|
||||
|
||||
**Update application**:
|
||||
```bash
|
||||
cd /opt/honey/honey-be
|
||||
git pull
|
||||
docker-compose -f docker-compose.inferno.yml up -d --build
|
||||
```
|
||||
|
||||
**Backup database**:
|
||||
```bash
|
||||
docker-compose -f docker-compose.inferno.yml exec db mysqldump -u honey_user -p honey_db > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The application supports two configuration strategies:
|
||||
|
||||
1. **Environment Variables** (Railway): Set variables in Railway dashboard
|
||||
2. **Secret File** (Inferno): Mount file at `/run/secrets/honey-config.properties`
|
||||
|
||||
Priority: Secret file → Environment variables
|
||||
|
||||
### Required Variables
|
||||
|
||||
- `SPRING_DATASOURCE_URL` - MySQL connection URL
|
||||
- `SPRING_DATASOURCE_USERNAME` - MySQL username
|
||||
- `SPRING_DATASOURCE_PASSWORD` - MySQL password
|
||||
- `TELEGRAM_BOT_TOKEN` - Telegram bot token for authentication
|
||||
- `FRONTEND_URL` - Frontend URL for CORS configuration
|
||||
|
||||
### Database Schema
|
||||
|
||||
The database uses **INT** and **BIGINT** only - no floating point numbers.
|
||||
|
||||
Current tables:
|
||||
- `users` - User information (id, telegram_id, username, created_at)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
- `GET /ping` - Health check (no auth required)
|
||||
- `GET /actuator/health` - Application health
|
||||
- `GET /actuator/health/readiness` - Readiness probe (checks database)
|
||||
- `GET /actuator/health/liveness` - Liveness probe
|
||||
|
||||
### Protected Endpoints (require Telegram auth)
|
||||
|
||||
- `GET /api/users/current` - Get current user information
|
||||
|
||||
## Authorization
|
||||
|
||||
The application uses Telegram Mini App authentication:
|
||||
|
||||
1. Frontend sends `Authorization: tma <initData>` header
|
||||
2. Backend validates Telegram signature
|
||||
3. Backend creates/updates user in database
|
||||
4. User is stored in thread-local context for the request
|
||||
|
||||
## Health Checks
|
||||
|
||||
- **Readiness**: `/actuator/health/readiness` - Checks database connectivity
|
||||
- **Liveness**: `/actuator/health/liveness` - Checks if application is running
|
||||
|
||||
Configure Railway to use `/actuator/health/readiness` as the health check path.
|
||||
|
||||
## Logging
|
||||
|
||||
- **Railway**: Built-in logging available in Railway dashboard
|
||||
- **Inferno**: Configure Grafana/Loki for log aggregation (see Grafana setup above)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
- Verify MySQL is running: `docker-compose ps`
|
||||
- Check connection credentials in environment variables
|
||||
- Review application logs for connection errors
|
||||
|
||||
### Authorization Failures
|
||||
|
||||
- Verify `TELEGRAM_BOT_TOKEN` is correct
|
||||
- Check that frontend is sending `Authorization: tma <initData>` header
|
||||
- Review backend logs for validation errors
|
||||
|
||||
### Deployment Issues
|
||||
|
||||
- Check health check endpoint: `curl http://your-url/actuator/health/readiness`
|
||||
- Review Railway deployment logs
|
||||
- Verify all environment variables are set correctly
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
### Building JAR
|
||||
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
### Running Locally with Docker
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
honey-be/
|
||||
├── src/
|
||||
│ ├── main/
|
||||
│ │ ├── java/com/honey/honey/
|
||||
│ │ │ ├── config/ # Configuration classes
|
||||
│ │ │ ├── controller/ # REST controllers
|
||||
│ │ │ ├── dto/ # Data transfer objects
|
||||
│ │ │ ├── exception/ # Exception handlers
|
||||
│ │ │ ├── health/ # Health indicators
|
||||
│ │ │ ├── logging/ # Grafana logging config
|
||||
│ │ │ ├── model/ # JPA entities
|
||||
│ │ │ ├── repository/ # JPA repositories
|
||||
│ │ │ ├── security/ # Auth interceptor, UserContext
|
||||
│ │ │ └── service/ # Business logic
|
||||
│ │ └── resources/
|
||||
│ │ ├── db/migration/ # Flyway migrations
|
||||
│ │ └── application.yml # Application config
|
||||
├── nginx/ # Nginx config (for Inferno)
|
||||
├── Dockerfile # Dockerfile for Railway
|
||||
├── Dockerfile.inferno # Dockerfile for Inferno
|
||||
├── docker-compose.yml # Docker Compose for local/Railway
|
||||
├── docker-compose.inferno.yml # Docker Compose for Inferno
|
||||
└── pom.xml # Maven configuration
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[Your License Here]
|
||||
|
||||
235
TELEGRAM_SETUP.md
Normal file
235
TELEGRAM_SETUP.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Telegram Bot and Mini App Setup Guide
|
||||
|
||||
This guide explains how to set up a Telegram bot and mini app for the Honey project.
|
||||
|
||||
## Step 1: Create a Telegram Bot
|
||||
|
||||
1. **Open Telegram** and search for **@BotFather**
|
||||
|
||||
2. **Start a conversation** with BotFather:
|
||||
```
|
||||
/start
|
||||
```
|
||||
|
||||
3. **Create a new bot**:
|
||||
```
|
||||
/newbot
|
||||
```
|
||||
|
||||
4. **Follow the prompts**:
|
||||
- Enter a name for your bot (e.g., "Honey Bot")
|
||||
- Enter a username for your bot (must end with `bot`, e.g., "honey_bot")
|
||||
|
||||
5. **Save the Bot Token**:
|
||||
- BotFather will provide you with a token like: `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz`
|
||||
- **IMPORTANT**: Keep this token secret! It's used to authenticate your bot
|
||||
- Add this token to your backend environment variables as `TELEGRAM_BOT_TOKEN`
|
||||
|
||||
## Step 2: Configure Bot Settings
|
||||
|
||||
1. **Set bot description** (optional):
|
||||
```
|
||||
/setdescription
|
||||
```
|
||||
Select your bot and enter a description.
|
||||
|
||||
2. **Set bot commands** (optional):
|
||||
```
|
||||
/setcommands
|
||||
```
|
||||
Select your bot and enter commands (e.g., `/start - Start the bot`)
|
||||
|
||||
3. **Set bot photo** (optional):
|
||||
```
|
||||
/setuserpic
|
||||
```
|
||||
Select your bot and upload a photo.
|
||||
|
||||
## Step 3: Create a Mini App
|
||||
|
||||
1. **Open BotFather** and select your bot
|
||||
|
||||
2. **Create a new mini app**:
|
||||
```
|
||||
/newapp
|
||||
```
|
||||
|
||||
3. **Follow the prompts**:
|
||||
- Select your bot
|
||||
- Enter a title for your mini app (e.g., "Honey")
|
||||
- Enter a short name (e.g., "honey")
|
||||
- Enter a description
|
||||
- Upload an icon (512x512 PNG, max 100KB)
|
||||
- Upload a photo (640x360 JPG/PNG, max 5MB) - optional
|
||||
- Upload a GIF (640x360, max 1MB) - optional
|
||||
|
||||
4. **Set the Mini App URL**:
|
||||
- BotFather will ask for the Web App URL
|
||||
- For **Railway deployment**: Use your Railway frontend URL
|
||||
```
|
||||
https://your-frontend.railway.app
|
||||
```
|
||||
- For **Inferno deployment**: Use your domain
|
||||
```
|
||||
https://your-domain.com
|
||||
```
|
||||
- For **local development**: Use a tunnel service like ngrok
|
||||
```
|
||||
https://your-ngrok-url.ngrok.io
|
||||
```
|
||||
|
||||
5. **Save the Mini App Link**:
|
||||
- BotFather will provide you with a link like: `https://t.me/your_bot/honey`
|
||||
- This is the link users will use to open your mini app
|
||||
|
||||
## Step 4: Configure Web App Settings
|
||||
|
||||
### For Railway Deployment
|
||||
|
||||
1. **Get your frontend URL** from Railway:
|
||||
- Go to your frontend service in Railway
|
||||
- Go to **"Settings"** → **"Networking"**
|
||||
- Copy the generated domain (e.g., `https://honey-fe-production.up.railway.app`)
|
||||
|
||||
2. **Update BotFather** with this URL:
|
||||
```
|
||||
/newapp
|
||||
```
|
||||
Select your bot and update the Web App URL
|
||||
|
||||
### For Inferno Deployment
|
||||
|
||||
1. **Get your domain** (e.g., `https://honey.yourdomain.com`)
|
||||
|
||||
2. **Update BotFather** with this URL:
|
||||
```
|
||||
/newapp
|
||||
```
|
||||
Select your bot and update the Web App URL
|
||||
|
||||
### For Local Development
|
||||
|
||||
1. **Use ngrok** to create a tunnel:
|
||||
```bash
|
||||
ngrok http 5173
|
||||
```
|
||||
|
||||
2. **Copy the HTTPS URL** (e.g., `https://abc123.ngrok.io`)
|
||||
|
||||
3. **Update BotFather** with this URL:
|
||||
```
|
||||
/newapp
|
||||
```
|
||||
Select your bot and update the Web App URL
|
||||
|
||||
4. **Update your frontend** `.env` file:
|
||||
```env
|
||||
VITE_API_BASE_URL=https://your-backend-url.railway.app
|
||||
```
|
||||
|
||||
## Step 5: Test the Mini App
|
||||
|
||||
1. **Open Telegram** and search for your bot (e.g., `@honey_bot`)
|
||||
|
||||
2. **Start the bot**:
|
||||
```
|
||||
/start
|
||||
```
|
||||
|
||||
3. **Open the mini app**:
|
||||
- Click the button in the bot chat (if you added one)
|
||||
- Or use the link: `https://t.me/your_bot/honey`
|
||||
- Or use the menu button (if configured)
|
||||
|
||||
4. **Verify authentication**:
|
||||
- The mini app should load your frontend
|
||||
- The frontend should successfully authenticate with the backend
|
||||
- Check browser console for any errors
|
||||
|
||||
## Step 6: Configure Bot Menu (Optional)
|
||||
|
||||
You can add a menu button to your bot that opens the mini app:
|
||||
|
||||
1. **Open BotFather**:
|
||||
```
|
||||
/mybots
|
||||
```
|
||||
|
||||
2. **Select your bot**:
|
||||
```
|
||||
Bot Settings → Menu Button
|
||||
```
|
||||
|
||||
3. **Configure menu button**:
|
||||
- Select "Configure Menu Button"
|
||||
- Enter button text (e.g., "Open Honey")
|
||||
- Enter the mini app URL or select from your apps
|
||||
|
||||
## Step 7: Security Considerations
|
||||
|
||||
1. **Keep Bot Token Secret**:
|
||||
- Never commit the bot token to version control
|
||||
- Use environment variables or secret files (as configured in the backend)
|
||||
|
||||
2. **Validate initData**:
|
||||
- The backend automatically validates Telegram's `initData` signature
|
||||
- This ensures requests are coming from legitimate Telegram users
|
||||
|
||||
3. **HTTPS Required**:
|
||||
- Telegram mini apps require HTTPS
|
||||
- Use Railway's automatic HTTPS or configure SSL on Inferno
|
||||
|
||||
## Step 8: Troubleshooting
|
||||
|
||||
### Mini App Not Loading
|
||||
|
||||
- **Check URL**: Verify the Web App URL in BotFather matches your frontend URL
|
||||
- **Check HTTPS**: Ensure your frontend is served over HTTPS
|
||||
- **Check CORS**: Verify backend CORS configuration includes Telegram domains
|
||||
- **Check Console**: Open browser developer tools and check for errors
|
||||
|
||||
### Authentication Failing
|
||||
|
||||
- **Check Bot Token**: Verify `TELEGRAM_BOT_TOKEN` is correct in backend environment
|
||||
- **Check initData**: Verify frontend is sending `Authorization: tma <initData>` header
|
||||
- **Check Backend Logs**: Review backend logs for authentication errors
|
||||
|
||||
### Bot Not Responding
|
||||
|
||||
- **Check Bot Status**: Verify bot is active in BotFather
|
||||
- **Check Commands**: Verify bot commands are set correctly
|
||||
- **Check Token**: Verify bot token is correct
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Telegram Bot API Documentation](https://core.telegram.org/bots/api)
|
||||
- [Telegram Mini Apps Documentation](https://core.telegram.org/bots/webapps)
|
||||
- [BotFather Commands](https://core.telegram.org/bots/tools#botfather)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### BotFather Commands
|
||||
|
||||
```
|
||||
/newbot - Create a new bot
|
||||
/setdescription - Set bot description
|
||||
/setcommands - Set bot commands
|
||||
/newapp - Create/update mini app
|
||||
/mybots - Manage your bots
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||
FRONTEND_URL=https://your-frontend-url.com
|
||||
```
|
||||
|
||||
### Testing Locally
|
||||
|
||||
1. Start backend: `mvn spring-boot:run` or `docker-compose up`
|
||||
2. Start frontend: `npm run dev`
|
||||
3. Use ngrok: `ngrok http 5173`
|
||||
4. Update BotFather with ngrok URL
|
||||
5. Test mini app in Telegram
|
||||
|
||||
68
docker-compose.inferno.yml
Normal file
68
docker-compose.inferno.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: honey-mysql
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_DATABASE: honey_db
|
||||
MYSQL_USER: honey_user
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- honey_mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- honey-network
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.inferno
|
||||
container_name: honey-backend
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/honey_db
|
||||
- SPRING_DATASOURCE_USERNAME=honey_user
|
||||
- SPRING_DATASOURCE_PASSWORD=${MYSQL_PASSWORD}
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
- FRONTEND_URL=${FRONTEND_URL}
|
||||
volumes:
|
||||
# Mount secret file from tmpfs
|
||||
- /run/secrets:/run/secrets:ro
|
||||
networks:
|
||||
- honey-network
|
||||
# Don't expose port directly - nginx will handle it
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: honey-nginx
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
# SSL certificates (if using HTTPS)
|
||||
# - ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- honey-network
|
||||
|
||||
volumes:
|
||||
honey_mysql_data:
|
||||
|
||||
networks:
|
||||
honey-network:
|
||||
driver: bridge
|
||||
|
||||
40
docker-compose.yml
Normal file
40
docker-compose.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: honey-mysql
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_DATABASE: ${DB_NAME:honey_db}
|
||||
MYSQL_USER: ${DB_USERNAME:root}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD:password}
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:password}
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- honey_mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD:password}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
env_file:
|
||||
- .env
|
||||
build: .
|
||||
container_name: honey-backend
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/${DB_NAME:honey_db}
|
||||
- SPRING_DATASOURCE_USERNAME=${DB_USERNAME:root}
|
||||
- SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD:password}
|
||||
|
||||
volumes:
|
||||
honey_mysql_data:
|
||||
|
||||
76
nginx/conf.d/honey.conf
Normal file
76
nginx/conf.d/honey.conf
Normal file
@@ -0,0 +1,76 @@
|
||||
upstream honey_backend {
|
||||
server app:8080;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Increase body size limit for API requests
|
||||
client_max_body_size 10M;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# API endpoints
|
||||
location /api/ {
|
||||
proxy_pass http://honey_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Actuator endpoints (for health checks)
|
||||
location /actuator/ {
|
||||
proxy_pass http://honey_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Ping endpoint
|
||||
location /ping {
|
||||
proxy_pass http://honey_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Frontend (if serving static files)
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
index index.html;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS configuration (uncomment and configure when SSL certificates are available)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name your-domain.com;
|
||||
#
|
||||
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
#
|
||||
# # SSL configuration
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
#
|
||||
# # Same location blocks as HTTP server
|
||||
# # ...
|
||||
# }
|
||||
|
||||
34
nginx/nginx.conf
Normal file
34
nginx/nginx.conf
Normal file
@@ -0,0 +1,34 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
||||
88
pom.xml
Normal file
88
pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.honey</groupId>
|
||||
<artifactId>honey-be</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<!-- Spring Boot Parent -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.0</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Actuator -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Tests -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Flyway -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Spring Boot Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
18
src/main/java/com/honey/honey/HoneyBackendApplication.java
Normal file
18
src/main/java/com/honey/honey/HoneyBackendApplication.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.honey.honey;
|
||||
|
||||
import com.honey.honey.config.ConfigLoader;
|
||||
import com.honey.honey.config.TelegramProperties;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties({TelegramProperties.class})
|
||||
public class HoneyBackendApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication app = new SpringApplication(HoneyBackendApplication.class);
|
||||
app.addListeners(new ConfigLoader());
|
||||
app.run(args);
|
||||
}
|
||||
}
|
||||
|
||||
82
src/main/java/com/honey/honey/config/ConfigLoader.java
Normal file
82
src/main/java/com/honey/honey/config/ConfigLoader.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Loads configuration from a mounted secret file (tmpfs) with fallback to environment variables.
|
||||
* This allows switching between Railway (env vars) and Inferno (mounted file) deployments.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Mounted file at /run/secrets/honey-config.properties (Inferno)
|
||||
* 2. Environment variables (Railway)
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConfigLoader implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
|
||||
|
||||
private static final String SECRET_FILE_PATH = "/run/secrets/honey-config.properties";
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
|
||||
ConfigurableEnvironment environment = event.getEnvironment();
|
||||
MutablePropertySources propertySources = environment.getPropertySources();
|
||||
|
||||
Map<String, Object> configProperties = new HashMap<>();
|
||||
|
||||
// Try to load from mounted file first (Inferno deployment)
|
||||
File secretFile = new File(SECRET_FILE_PATH);
|
||||
if (secretFile.exists() && secretFile.isFile() && secretFile.canRead()) {
|
||||
log.info("📁 Loading configuration from mounted secret file: {}", SECRET_FILE_PATH);
|
||||
try {
|
||||
Properties props = new Properties();
|
||||
try (FileInputStream fis = new FileInputStream(secretFile)) {
|
||||
props.load(fis);
|
||||
}
|
||||
|
||||
for (String key : props.stringPropertyNames()) {
|
||||
String value = props.getProperty(key);
|
||||
configProperties.put(key, value);
|
||||
log.debug("Loaded from file: {} = {}", key, maskSensitiveValue(key, value));
|
||||
}
|
||||
log.info("✅ Successfully loaded {} properties from secret file", configProperties.size());
|
||||
} catch (IOException e) {
|
||||
log.warn("⚠️ Failed to read secret file, falling back to environment variables: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
log.info("📝 Secret file not found at {}, using environment variables", SECRET_FILE_PATH);
|
||||
}
|
||||
|
||||
// Environment variables are already loaded by Spring Boot by default
|
||||
// We just add file-based config as a higher priority source if it exists
|
||||
if (!configProperties.isEmpty()) {
|
||||
propertySources.addFirst(new MapPropertySource("secretFileConfig", configProperties));
|
||||
log.info("✅ Configuration loaded: {} properties from file, environment variables as fallback",
|
||||
configProperties.size());
|
||||
} else {
|
||||
log.info("✅ Using environment variables for configuration");
|
||||
}
|
||||
}
|
||||
|
||||
private String maskSensitiveValue(String key, String value) {
|
||||
if (value == null) return "null";
|
||||
if (key.toLowerCase().contains("password") ||
|
||||
key.toLowerCase().contains("token") ||
|
||||
key.toLowerCase().contains("secret") ||
|
||||
key.toLowerCase().contains("key")) {
|
||||
return value.length() > 4 ? value.substring(0, 2) + "***" + value.substring(value.length() - 2) : "***";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
37
src/main/java/com/honey/honey/config/CorsConfig.java
Normal file
37
src/main/java/com/honey/honey/config/CorsConfig.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Value("${FRONTEND_URL:https://example.com}")
|
||||
private String frontendUrl;
|
||||
|
||||
@Bean
|
||||
public WebMvcConfigurer corsConfigurer() {
|
||||
return new WebMvcConfigurer() {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(
|
||||
frontendUrl,
|
||||
"https://web.telegram.org",
|
||||
"https://webk.telegram.org",
|
||||
"https://t.me",
|
||||
"https://*.t.me"
|
||||
)
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
13
src/main/java/com/honey/honey/config/TelegramProperties.java
Normal file
13
src/main/java/com/honey/honey/config/TelegramProperties.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "telegram")
|
||||
@Data
|
||||
public class TelegramProperties {
|
||||
private String botToken;
|
||||
}
|
||||
|
||||
20
src/main/java/com/honey/honey/config/WebConfig.java
Normal file
20
src/main/java/com/honey/honey/config/WebConfig.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import com.honey.honey.security.AuthInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final AuthInterceptor authInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.excludePathPatterns("/ping", "/actuator/**"); // ping and actuator don't require auth
|
||||
}
|
||||
}
|
||||
|
||||
19
src/main/java/com/honey/honey/controller/PingController.java
Normal file
19
src/main/java/com/honey/honey/controller/PingController.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class PingController {
|
||||
|
||||
@GetMapping("/ping")
|
||||
public Map<String, String> ping() {
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("status", "ok");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
28
src/main/java/com/honey/honey/controller/UserController.java
Normal file
28
src/main/java/com/honey/honey/controller/UserController.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.UserDto;
|
||||
import com.honey.honey.model.User;
|
||||
import com.honey.honey.security.UserContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
@GetMapping("/current")
|
||||
public UserDto getCurrentUser() {
|
||||
User user = UserContext.get();
|
||||
|
||||
return UserDto.builder()
|
||||
.telegram_id(user.getTelegramId())
|
||||
.username(user.getUsername())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
16
src/main/java/com/honey/honey/dto/UserDto.java
Normal file
16
src/main/java/com/honey/honey/dto/UserDto.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDto {
|
||||
private Long telegram_id;
|
||||
private String username;
|
||||
}
|
||||
|
||||
14
src/main/java/com/honey/honey/exception/ErrorResponse.java
Normal file
14
src/main/java/com/honey/honey/exception/ErrorResponse.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.honey.honey.exception;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ErrorResponse {
|
||||
private String code;
|
||||
private String message;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.honey.honey.exception;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(UnauthorizedException.class)
|
||||
public ResponseEntity<ErrorResponse> handleUnauthorized(UnauthorizedException ex) {
|
||||
log.warn("Unauthorized: {}", ex.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(new ErrorResponse("UNAUTHORIZED", ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
|
||||
log.error("Unexpected error", ex);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.honey.honey.exception;
|
||||
|
||||
public class UnauthorizedException extends RuntimeException {
|
||||
public UnauthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.honey.honey.health;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DatabaseHealthIndicator implements HealthIndicator {
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
@Override
|
||||
public Health health() {
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
if (connection.isValid(1)) {
|
||||
return Health.up()
|
||||
.withDetail("database", "MySQL")
|
||||
.withDetail("status", "Connected")
|
||||
.build();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
log.error("Database health check failed", e);
|
||||
return Health.down()
|
||||
.withDetail("database", "MySQL")
|
||||
.withDetail("error", e.getMessage())
|
||||
.build();
|
||||
}
|
||||
return Health.down().withDetail("database", "MySQL").build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.honey.honey.logging;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* Configuration for Grafana integration.
|
||||
* This class prepares the logging infrastructure for Grafana.
|
||||
*
|
||||
* In production (Inferno), logs will be sent to Grafana via:
|
||||
* - Loki (log aggregation)
|
||||
* - Prometheus (metrics)
|
||||
*
|
||||
* For now, this is a placeholder that ensures structured logging
|
||||
* is ready for Grafana integration.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class GrafanaLoggingConfig {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
log.info("📊 Grafana logging configuration initialized");
|
||||
log.info("📊 Logs are structured and ready for Grafana/Loki integration");
|
||||
log.info("📊 Metrics will be available for Prometheus when configured");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log structured data for Grafana.
|
||||
* This method can be used to send custom logs to Grafana/Loki.
|
||||
*
|
||||
* @param level Log level (INFO, WARN, ERROR, etc.)
|
||||
* @param message Log message
|
||||
* @param metadata Additional metadata as key-value pairs
|
||||
*/
|
||||
public static void logToGrafana(String level, String message, java.util.Map<String, Object> metadata) {
|
||||
// For now, just use standard logging
|
||||
// In production, this will send logs to Grafana/Loki
|
||||
switch (level.toUpperCase()) {
|
||||
case "ERROR":
|
||||
log.error("{} | Metadata: {}", message, metadata);
|
||||
break;
|
||||
case "WARN":
|
||||
log.warn("{} | Metadata: {}", message, metadata);
|
||||
break;
|
||||
case "INFO":
|
||||
default:
|
||||
log.info("{} | Metadata: {}", message, metadata);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
src/main/java/com/honey/honey/model/User.java
Normal file
37
src/main/java/com/honey/honey/model/User.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.honey.honey.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "telegram_id", unique = true, nullable = false)
|
||||
private Long telegramId;
|
||||
|
||||
@Column(name = "username")
|
||||
private String username;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/main/java/com/honey/honey/repository/UserRepository.java
Normal file
15
src/main/java/com/honey/honey/repository/UserRepository.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.honey.honey.repository;
|
||||
|
||||
import com.honey.honey.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
List<User> findAllByTelegramId(Long telegramId);
|
||||
Optional<User> findByTelegramId(Long telegramId);
|
||||
}
|
||||
|
||||
118
src/main/java/com/honey/honey/security/AuthInterceptor.java
Normal file
118
src/main/java/com/honey/honey/security/AuthInterceptor.java
Normal file
@@ -0,0 +1,118 @@
|
||||
package com.honey.honey.security;
|
||||
|
||||
import com.honey.honey.model.User;
|
||||
import com.honey.honey.repository.UserRepository;
|
||||
import com.honey.honey.service.TelegramAuthService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final TelegramAuthService telegramAuthService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
|
||||
|
||||
// Allow CORS preflight (OPTIONS) without auth
|
||||
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get initData from Authorization header (format: "tma <initData>")
|
||||
String authHeader = req.getHeader("Authorization");
|
||||
String initData = null;
|
||||
|
||||
if (authHeader != null && authHeader.startsWith("tma ")) {
|
||||
initData = authHeader.substring(4); // Remove "tma " prefix
|
||||
}
|
||||
|
||||
// If no initData, fail
|
||||
if (initData == null || initData.isBlank()) {
|
||||
log.error("❌ Missing Telegram initData in Authorization header (expected format: 'tma <initData>')");
|
||||
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate Telegram digital signature
|
||||
Map<String, Object> tgUser = telegramAuthService.validateAndParseInitData(initData);
|
||||
|
||||
Long telegramId = ((Number) tgUser.get("id")).longValue();
|
||||
String username = (String) tgUser.get("username");
|
||||
|
||||
// Get or create user (with duplicate handling)
|
||||
User user = getOrCreateUser(telegramId, username);
|
||||
|
||||
// Put user in context
|
||||
UserContext.set(user);
|
||||
log.debug("🔑 Authenticated userId={}, telegramId={}", user.getId(), telegramId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets existing user or creates a new one. Handles race conditions and duplicate users gracefully.
|
||||
*/
|
||||
private synchronized User getOrCreateUser(Long telegramId, String username) {
|
||||
// Try to find existing user(s)
|
||||
List<User> existingUsers = userRepository.findAllByTelegramId(telegramId);
|
||||
|
||||
if (!existingUsers.isEmpty()) {
|
||||
User user = existingUsers.get(0);
|
||||
|
||||
// If multiple users exist, log a warning
|
||||
if (existingUsers.size() > 1) {
|
||||
log.warn("⚠️ Found {} duplicate users for telegramId={}. Using the first one (id={}). " +
|
||||
"Consider cleaning up duplicates.", existingUsers.size(), telegramId, user.getId());
|
||||
}
|
||||
|
||||
// Update username if it changed
|
||||
if (username != null && !username.equals(user.getUsername())) {
|
||||
user.setUsername(username);
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// No user found, create new one
|
||||
try {
|
||||
log.info("🆕 Creating new user for telegramId={}, username={}", telegramId, username);
|
||||
return userRepository.save(
|
||||
User.builder()
|
||||
.telegramId(telegramId)
|
||||
.username(username)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build()
|
||||
);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
// Another thread created the user, fetch it
|
||||
log.debug("User already exists (created by another thread), fetching...");
|
||||
List<User> users = userRepository.findAllByTelegramId(telegramId);
|
||||
if (users.isEmpty()) {
|
||||
log.error("Failed to create user and couldn't find it after duplicate key error");
|
||||
throw new RuntimeException("Failed to get or create user", e);
|
||||
}
|
||||
return users.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
|
||||
UserContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
21
src/main/java/com/honey/honey/security/UserContext.java
Normal file
21
src/main/java/com/honey/honey/security/UserContext.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.honey.honey.security;
|
||||
|
||||
import com.honey.honey.model.User;
|
||||
|
||||
public class UserContext {
|
||||
|
||||
private static final ThreadLocal<User> current = new ThreadLocal<>();
|
||||
|
||||
public static void set(User user) {
|
||||
current.set(user);
|
||||
}
|
||||
|
||||
public static User get() {
|
||||
return current.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
current.remove();
|
||||
}
|
||||
}
|
||||
|
||||
179
src/main/java/com/honey/honey/service/TelegramAuthService.java
Normal file
179
src/main/java/com/honey/honey/service/TelegramAuthService.java
Normal file
@@ -0,0 +1,179 @@
|
||||
package com.honey.honey.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.honey.honey.config.TelegramProperties;
|
||||
import com.honey.honey.exception.UnauthorizedException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TelegramAuthService {
|
||||
|
||||
private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
|
||||
private static final String WEB_APP_DATA_CONSTANT = "WebAppData";
|
||||
|
||||
private final TelegramProperties telegramProperties;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* Validates and parses Telegram initData string.
|
||||
*/
|
||||
public Map<String, Object> validateAndParseInitData(String initData) {
|
||||
|
||||
if (initData == null || initData.isBlank()) {
|
||||
throw new UnauthorizedException("Telegram initData is missing");
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1. Parse query string into key/value pairs.
|
||||
Map<String, String> parsedData = parseQueryString(initData);
|
||||
|
||||
String receivedHash = parsedData.remove("hash");
|
||||
if (receivedHash == null) {
|
||||
throw new UnauthorizedException("Missing Telegram hash");
|
||||
}
|
||||
|
||||
// Step 2. Build data check string.
|
||||
String dataCheckString = createDataCheckString(parsedData);
|
||||
|
||||
// Step 3. Derive secret key based on Telegram WebApp rules.
|
||||
byte[] secretKey = deriveSecretKey(telegramProperties.getBotToken());
|
||||
|
||||
// Step 4. Calculate our own hash and compare.
|
||||
String calculatedHash = calculateHmacSha256(dataCheckString, secretKey);
|
||||
|
||||
if (!receivedHash.equals(calculatedHash)) {
|
||||
log.warn("Telegram signature mismatch. Expected={}, Received={}", calculatedHash, receivedHash);
|
||||
throw new UnauthorizedException("Invalid Telegram signature");
|
||||
}
|
||||
|
||||
// Step 5. Extract the user JSON from initData.
|
||||
Map<String, String> decoded = decodeQueryParams(initData);
|
||||
String userJson = decoded.get("user");
|
||||
|
||||
if (userJson == null) {
|
||||
throw new UnauthorizedException("initData does not contain 'user' field");
|
||||
}
|
||||
|
||||
// Step 6. Parse JSON into map.
|
||||
return objectMapper.readValue(userJson, Map.class);
|
||||
|
||||
} catch (UnauthorizedException ex) {
|
||||
throw ex;
|
||||
|
||||
} catch (Exception ex) {
|
||||
log.error("Telegram initData validation failed: {}", ex.getMessage(), ex);
|
||||
throw new UnauthorizedException("Invalid Telegram initData");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Internal helper methods
|
||||
// -------------------------------------------
|
||||
|
||||
private Map<String, String> parseQueryString(String queryString) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
|
||||
try {
|
||||
String[] pairs = queryString.split("&");
|
||||
|
||||
for (String pair : pairs) {
|
||||
int idx = pair.indexOf("=");
|
||||
|
||||
if (idx <= 0) continue;
|
||||
|
||||
String key = URLDecoder.decode(pair.substring(0, idx), UTF_8);
|
||||
String value = URLDecoder.decode(pair.substring(idx + 1), UTF_8);
|
||||
|
||||
params.put(key, value);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to parse initData query string: {}", ex.getMessage());
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
private String createDataCheckString(Map<String, String> data) {
|
||||
List<String> sortedKeys = new ArrayList<>(data.keySet());
|
||||
Collections.sort(sortedKeys);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (String key : sortedKeys) {
|
||||
if (sb.length() > 0) sb.append("\n");
|
||||
sb.append(key).append("=").append(data.get(key));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private byte[] deriveSecretKey(String botToken) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac hmacSha256 = Mac.getInstance(HMAC_SHA256_ALGORITHM);
|
||||
|
||||
// Telegram requires using "WebAppData" as the key for deriving secret
|
||||
SecretKeySpec secretKeySpec =
|
||||
new SecretKeySpec(WEB_APP_DATA_CONSTANT.getBytes(StandardCharsets.UTF_8), HMAC_SHA256_ALGORITHM);
|
||||
|
||||
hmacSha256.init(secretKeySpec);
|
||||
|
||||
return hmacSha256.doFinal(botToken.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String calculateHmacSha256(String data, byte[] key)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
|
||||
Mac hmacSha256 = Mac.getInstance(HMAC_SHA256_ALGORITHM);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM);
|
||||
|
||||
hmacSha256.init(secretKeySpec);
|
||||
|
||||
byte[] hashBytes = hmacSha256.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return bytesToHex(hashBytes);
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hexString = new StringBuilder(bytes.length * 2);
|
||||
|
||||
for (byte b : bytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
|
||||
if (hex.length() == 1) hexString.append('0');
|
||||
|
||||
hexString.append(hex);
|
||||
}
|
||||
|
||||
return hexString.toString();
|
||||
}
|
||||
|
||||
private Map<String, String> decodeQueryParams(String qs) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
|
||||
for (String part : qs.split("&")) {
|
||||
int idx = part.indexOf('=');
|
||||
|
||||
if (idx > 0) {
|
||||
String key = URLDecoder.decode(part.substring(0, idx), UTF_8);
|
||||
String val = URLDecoder.decode(part.substring(idx + 1), UTF_8);
|
||||
map.put(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
68
src/main/resources/application.yml
Normal file
68
src/main/resources/application.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: honey-be
|
||||
|
||||
datasource:
|
||||
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/honey_db}
|
||||
username: ${SPRING_DATASOURCE_USERNAME:root}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:password}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
connection-timeout: 30000
|
||||
initialization-fail-timeout: -1
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: false
|
||||
dialect: org.hibernate.dialect.MySQLDialect
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
connect-retries: 20
|
||||
connect-retry-interval: 3000
|
||||
validate-on-migrate: false
|
||||
repair: true
|
||||
|
||||
telegram:
|
||||
bot-token: ${TELEGRAM_BOT_TOKEN}
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org.springframework.boot.context.config: DEBUG
|
||||
org.springframework.core.env: DEBUG
|
||||
com.honey: DEBUG
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when-authorized
|
||||
probes:
|
||||
enabled: true
|
||||
group:
|
||||
readiness:
|
||||
include: db,ping
|
||||
liveness:
|
||||
include: ping
|
||||
health:
|
||||
db:
|
||||
enabled: true
|
||||
diskspace:
|
||||
enabled: false
|
||||
ping:
|
||||
enabled: true
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Create users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
telegram_id BIGINT NOT NULL UNIQUE,
|
||||
username VARCHAR(255),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_telegram_id (telegram_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
Reference in New Issue
Block a user