Created base BE. Auth + /current endpoint
This commit is contained in:
60
.gitignore
vendored
60
.gitignore
vendored
@@ -1,24 +1,40 @@
|
|||||||
# Compiled class file
|
target/
|
||||||
*.class
|
!.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
|
*.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