finish the billing stuff on the backend
This commit is contained in:
parent
3be0ede054
commit
46e3901470
390
README.md
390
README.md
@ -1,148 +1,212 @@
|
||||
# Termcloud
|
||||
# TermCloud Storage Platform
|
||||
|
||||
A Mullvad-style file storage service with Bitcoin payments and usage-based billing.
|
||||
A cloud storage platform with Mullvad-style anonymity, usage-based billing, and S3-compatible bucket policies.
|
||||
|
||||
## Features
|
||||
## Quick Start
|
||||
|
||||
- **Account-based System**: Anonymous account creation with 16-digit account numbers
|
||||
- **Bitcoin Payments**: Pay with Bitcoin to activate and fund your account
|
||||
- **Usage-based Billing**: Charged monthly for peak storage usage (pay for what you use)
|
||||
- **Bucket Policies**: AWS S3-compatible JSON policies for access control
|
||||
- **RESTful API**: Complete REST API for all operations
|
||||
### 1. Set up the Server
|
||||
|
||||
```bash
|
||||
git clone https://git.keircn.com/keiran/termcloud
|
||||
cd termcloud
|
||||
psql -d termcloud -f internal/db/schema.sql
|
||||
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
|
||||
make build
|
||||
./termcloud
|
||||
```
|
||||
|
||||
### 2. Install and Use CLI
|
||||
|
||||
```bash
|
||||
go build -o tcman cmd/tcman/main.go
|
||||
|
||||
# Create account
|
||||
./tcman account create
|
||||
|
||||
# Configure server (if not localhost:8080)
|
||||
./tcman config set server-url https://your-server.com
|
||||
|
||||
# Add funds to activate account
|
||||
./tcman account top-up 10.00
|
||||
|
||||
# Check account status
|
||||
./tcman account info
|
||||
```
|
||||
|
||||
## CLI Usage (`tcman`)
|
||||
|
||||
### Account Management
|
||||
|
||||
```bash
|
||||
# Create new account
|
||||
tcman account create
|
||||
|
||||
# Login with existing credentials
|
||||
tcman account login 1234567890123456 your-access-token
|
||||
|
||||
# View account information
|
||||
tcman account info
|
||||
|
||||
# Add funds via Bitcoin
|
||||
tcman account top-up 25.00
|
||||
|
||||
# Check payment history
|
||||
tcman account payments
|
||||
|
||||
# View usage and billing info
|
||||
tcman account usage
|
||||
```
|
||||
|
||||
### Bucket Management
|
||||
|
||||
```bash
|
||||
# List all buckets
|
||||
tcman bucket list
|
||||
|
||||
# Create new bucket
|
||||
tcman bucket create my-files
|
||||
|
||||
# Get bucket information
|
||||
tcman bucket info my-files
|
||||
|
||||
# Delete bucket (with confirmation)
|
||||
tcman bucket delete my-files
|
||||
|
||||
# Delete bucket without confirmation
|
||||
tcman bucket delete my-files --force
|
||||
```
|
||||
|
||||
### File Operations
|
||||
|
||||
```bash
|
||||
# Upload file
|
||||
tcman file upload my-files ./local-file.txt
|
||||
|
||||
# Upload file with custom remote name
|
||||
tcman file upload my-files ./local-file.txt remote-name.txt
|
||||
|
||||
# Upload with custom content type
|
||||
tcman file upload my-files ./image.jpg --content-type image/jpeg
|
||||
|
||||
# Download file
|
||||
tcman file download my-files remote-file.txt
|
||||
|
||||
# Download file with custom local name
|
||||
tcman file download my-files remote-file.txt ./local-file.txt
|
||||
|
||||
# List files in bucket
|
||||
tcman file list my-files
|
||||
|
||||
# Delete file
|
||||
tcman file delete my-files remote-file.txt
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Show current configuration
|
||||
tcman config show
|
||||
|
||||
# Set server URL
|
||||
tcman config set server-url https://api.termcloud.com
|
||||
|
||||
# Reset configuration (logout and clear settings)
|
||||
tcman config reset
|
||||
```
|
||||
|
||||
## Authentication System
|
||||
|
||||
Similar to Mullvad VPN:
|
||||
1. **Create Account**: Generate anonymous 16-digit account number + access token
|
||||
2. **Add Funds**: Pay ~$5 worth of Bitcoin to activate account
|
||||
3. **Usage Billing**: Charged monthly based on peak storage usage ($0.50/GB default)
|
||||
2. **Add Funds**: Pay $5+ worth of Bitcoin to activate account
|
||||
3. **Usage Billing**: Charged monthly for peak storage usage ($0.50/GB default)
|
||||
4. **No Personal Info**: No emails, usernames, or personal information required
|
||||
|
||||
## Setup
|
||||
|
||||
1. Set up PostgreSQL database and run the schema:
|
||||
```bash
|
||||
psql -d termcloud -f internal/db/schema.sql
|
||||
```
|
||||
|
||||
2. Configure environment variables (copy `.env.example` to `.env` and customize):
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
## Server Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| ------------------ | ------- | ------------------------------------- |
|
||||
| `PRICE_PER_GB_USD` | 0.50 | Monthly charge per GB of peak storage |
|
||||
| `DATABASE_URL` | - | PostgreSQL connection string |
|
||||
| `PORT` | 8080 | Server port |
|
||||
| `STORAGE_DIR` | storage | Directory for file storage |
|
||||
| `MAX_FILE_SIZE` | 100MB | Maximum file upload size |
|
||||
| `RATE_LIMIT` | 100 | Requests per second per IP |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Account Management
|
||||
|
||||
3. Build and run:
|
||||
```bash
|
||||
make build
|
||||
make run
|
||||
# Create account (public endpoint)
|
||||
POST /api/accounts
|
||||
|
||||
# Get account info (requires auth)
|
||||
GET /api/account
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# Create Bitcoin payment
|
||||
POST /api/payments
|
||||
Headers: X-Access-Token: your-token
|
||||
Body: {"usd_amount": 10.00}
|
||||
|
||||
# Check payment status
|
||||
GET /api/payments/123
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# List all payments
|
||||
GET /api/payments
|
||||
Headers: X-Access-Token: your-token
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Bucket Operations
|
||||
|
||||
### 1. Create Account
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/accounts
|
||||
```
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"accountNumber": "1234567890123456",
|
||||
"accessToken": "abc123...",
|
||||
"balanceUsd": 0.00,
|
||||
"isActive": false,
|
||||
"message": "Account created. Add funds to activate."
|
||||
}
|
||||
# List buckets
|
||||
GET /api/buckets
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# Create bucket
|
||||
PUT /api/buckets/my-bucket
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# Get bucket info
|
||||
GET /api/buckets/my-bucket
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# Delete bucket
|
||||
DELETE /api/buckets/my-bucket
|
||||
Headers: X-Access-Token: your-token
|
||||
```
|
||||
|
||||
### 2. Add Funds (Bitcoin Payment)
|
||||
### Object Operations
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/account/payments \
|
||||
-H "X-Access-Token: your-access-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"amount": 5.00}'
|
||||
# Upload file
|
||||
PUT /api/buckets/my-bucket/objects/file.txt
|
||||
Headers: X-Access-Token: your-token
|
||||
Headers: Content-Type: text/plain
|
||||
Body: [file contents]
|
||||
|
||||
# Download file
|
||||
GET /api/buckets/my-bucket/objects/file.txt
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# List objects
|
||||
GET /api/buckets/my-bucket/objects
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# Delete object
|
||||
DELETE /api/buckets/my-bucket/objects/file.txt
|
||||
Headers: X-Access-Token: your-token
|
||||
```
|
||||
|
||||
### 3. Use Storage (requires active account)
|
||||
All storage endpoints require `X-Access-Token` header:
|
||||
## Bucket Policies (S3-Compatible)
|
||||
|
||||
### API Endpoints
|
||||
Set access control policies using AWS S3-compatible JSON syntax:
|
||||
|
||||
All API endpoints require `X-API-Key` header.
|
||||
|
||||
**Buckets:**
|
||||
|
||||
- `GET /api/v1/buckets` - List user buckets
|
||||
- `POST /api/v1/buckets` - Create bucket `{"name": "my-bucket"}`
|
||||
- `DELETE /api/v1/buckets/:bucket` - Delete bucket
|
||||
|
||||
**Objects:**
|
||||
|
||||
- `GET /api/v1/buckets/:bucket/objects` - List objects in bucket
|
||||
- `PUT /api/v1/buckets/:bucket/objects/*` - Upload file (multipart form with "file" field)
|
||||
- `GET /api/v1/buckets/:bucket/objects/*` - Download file
|
||||
- `DELETE /api/v1/buckets/:bucket/objects/*` - Delete file
|
||||
|
||||
**User Info:**
|
||||
|
||||
- `GET /api/v1/user` - Get user info and usage stats
|
||||
|
||||
**Bucket Policies:**
|
||||
- `PUT /api/v1/buckets/:bucket/policy` - Set bucket policy `{"policy": "json-policy-string"}`
|
||||
- `GET /api/v1/buckets/:bucket/policy` - Get bucket policy
|
||||
- `DELETE /api/v1/buckets/:bucket/policy` - Delete bucket policy
|
||||
|
||||
## Bucket Policies
|
||||
|
||||
Bucket policies use JSON format similar to AWS S3 IAM policies to control access to buckets and objects.
|
||||
|
||||
### Policy Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "StatementId",
|
||||
"Effect": "Allow|Deny",
|
||||
"Principal": {
|
||||
"User": ["username1", "username2"]
|
||||
},
|
||||
"Action": [
|
||||
"termcloud:GetObject",
|
||||
"termcloud:PutObject",
|
||||
"termcloud:DeleteObject",
|
||||
"termcloud:ListObjects"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:termcloud:s3:::bucket-name/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Actions
|
||||
|
||||
- `termcloud:GetObject` - Download files
|
||||
- `termcloud:PutObject` - Upload files
|
||||
- `termcloud:DeleteObject` - Delete files
|
||||
- `termcloud:ListObjects` - List files in bucket
|
||||
- `termcloud:GetBucket` - Get bucket info
|
||||
- `termcloud:DeleteBucket` - Delete bucket
|
||||
- `*` - All actions
|
||||
|
||||
### Policy Examples
|
||||
|
||||
**Read-only access:**
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
@ -157,37 +221,73 @@ Bucket policies use JSON format similar to AWS S3 IAM policies to control access
|
||||
}
|
||||
```
|
||||
|
||||
**Deny delete operations:**
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Principal": {"User": ["*"]},
|
||||
"Action": ["termcloud:DeleteObject"],
|
||||
"Resource": ["arn:termcloud:s3:::my-bucket/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
### Supported Actions
|
||||
|
||||
### Example Usage
|
||||
- `termcloud:GetObject` - Download files
|
||||
- `termcloud:PutObject` - Upload files
|
||||
- `termcloud:DeleteObject` - Delete files
|
||||
- `termcloud:ListObjects` - List files
|
||||
- `termcloud:GetBucket` - Get bucket info
|
||||
- `termcloud:DeleteBucket` - Delete bucket
|
||||
|
||||
### Policy Management
|
||||
|
||||
```bash
|
||||
# Create bucket
|
||||
curl -X POST http://localhost:8080/api/v1/buckets \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "my-files"}'
|
||||
# Set bucket policy
|
||||
PUT /api/buckets/my-bucket/policy
|
||||
Headers: X-Access-Token: your-token
|
||||
Body: [JSON policy]
|
||||
|
||||
# Upload file
|
||||
curl -X PUT http://localhost:8080/api/v1/buckets/my-files/objects/test.txt \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-F "file=@test.txt"
|
||||
# Get bucket policy
|
||||
GET /api/buckets/my-bucket/policy
|
||||
Headers: X-Access-Token: your-token
|
||||
|
||||
# Download file
|
||||
curl http://localhost:8080/api/v1/buckets/my-files/objects/test.txt \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-o downloaded.txt
|
||||
# Delete bucket policy
|
||||
DELETE /api/buckets/my-bucket/policy
|
||||
Headers: X-Access-Token: your-token
|
||||
```
|
||||
|
||||
## Billing System
|
||||
|
||||
- **Activation**: $5.00 minimum Bitcoin payment required
|
||||
- **Monthly Billing**: Charged on 1st of each month for previous month's peak usage
|
||||
- **Rate**: $0.50 per GB per month (configurable)
|
||||
- **Usage Tracking**: Automatic tracking of peak storage across all buckets
|
||||
- **Account Deactivation**: Account becomes inactive when balance reaches $0
|
||||
|
||||
### Payment Flow
|
||||
|
||||
1. Create payment request with desired USD amount
|
||||
2. System generates Bitcoin address and calculates BTC amount
|
||||
3. Send exact BTC amount to provided address
|
||||
4. System confirms payment and credits account
|
||||
5. Account activates and storage becomes available
|
||||
|
||||
## Security Features
|
||||
|
||||
- **Anonymous Accounts**: No personal information required
|
||||
- **Access Tokens**: Cryptographically secure 256-bit access tokens
|
||||
- **Resource Limits**: Prevents uploads exceeding account balance capacity
|
||||
- **Rate Limiting**: Configurable request rate limits
|
||||
- **Bucket Policies**: Granular access control for shared storage
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
# Build server
|
||||
go build -o termcloud cmd/termcloud/main.go
|
||||
|
||||
# Build CLI
|
||||
go build -o tcman cmd/tcman/main.go
|
||||
|
||||
# Run tests
|
||||
go test ./...
|
||||
|
||||
# Format code
|
||||
go fmt ./...
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
|
||||
@ -44,17 +44,20 @@ func main() {
|
||||
|
||||
e.GET("/", h.RootHandler)
|
||||
|
||||
e.POST("/api/v1/accounts", h.CreateAccountHandler)
|
||||
e.POST("/api/accounts", h.CreateAccountHandler)
|
||||
|
||||
api := e.Group("/api/v1")
|
||||
api := e.Group("/api")
|
||||
api.Use(h.AuthMiddleware)
|
||||
|
||||
api.GET("/account", h.GetAccountHandler)
|
||||
api.POST("/account/payments", h.CreatePaymentHandler)
|
||||
api.POST("/account/payments/:paymentId/confirm", h.ConfirmPaymentHandler)
|
||||
api.POST("/payments", h.CreatePaymentHandler)
|
||||
api.GET("/payments", h.GetPaymentsHandler)
|
||||
api.GET("/payments/:id", h.GetPaymentHandler)
|
||||
api.POST("/payments/:id/confirm", h.ConfirmPaymentHandler)
|
||||
|
||||
api.GET("/buckets", h.ListBucketsHandler)
|
||||
api.POST("/buckets", h.CreateBucketHandler)
|
||||
api.PUT("/buckets/:bucket", h.CreateBucketHandler)
|
||||
api.GET("/buckets/:bucket", h.GetBucketHandler)
|
||||
api.DELETE("/buckets/:bucket", h.DeleteBucketHandler)
|
||||
|
||||
api.PUT("/buckets/:bucket/policy", h.SetBucketPolicyHandler)
|
||||
@ -62,7 +65,7 @@ func main() {
|
||||
api.DELETE("/buckets/:bucket/policy", h.DeleteBucketPolicyHandler)
|
||||
|
||||
api.GET("/buckets/:bucket/objects", h.ListObjectsHandler)
|
||||
api.PUT("/buckets/:bucket/objects/*", h.UploadObjectHandler)
|
||||
api.PUT("/buckets/:bucket/objects/*", h.PutObjectHandler)
|
||||
api.GET("/buckets/:bucket/objects/*", h.GetObjectHandler)
|
||||
api.DELETE("/buckets/:bucket/objects/*", h.DeleteObjectHandler)
|
||||
|
||||
|
||||
@ -32,10 +32,17 @@ type Payment struct {
|
||||
Confirmations int `json:"confirmations"`
|
||||
TxHash string `json:"txHash,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ConfirmedAt *time.Time `json:"confirmedAt,omitempty"`
|
||||
}
|
||||
|
||||
type CryptoRate struct {
|
||||
Currency string `json:"currency"`
|
||||
USDRate float64 `json:"usdRate"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type UsageRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
AccountID int64 `json:"accountId"`
|
||||
@ -141,22 +148,23 @@ func (s *AccountService) GetAccountByNumber(ctx context.Context, accountNumber s
|
||||
}
|
||||
|
||||
func (s *AccountService) CreatePayment(ctx context.Context, accountID int64, usdAmount float64) (*Payment, error) {
|
||||
btcPrice, err := s.getBTCPrice()
|
||||
btcPrice, err := s.getBTCPrice(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get BTC price: %w", err)
|
||||
}
|
||||
|
||||
btcAmount := usdAmount / btcPrice
|
||||
btcAddress := s.generateBTCAddress()
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
var payment Payment
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
INSERT INTO payments (account_id, payment_type, btc_address, btc_amount, usd_amount, status)
|
||||
VALUES ($1, 'bitcoin', $2, $3, $4, 'pending')
|
||||
RETURNING id, account_id, payment_type, btc_address, btc_amount, usd_amount, confirmations, status, created_at`,
|
||||
accountID, btcAddress, btcAmount, usdAmount).Scan(
|
||||
INSERT INTO payments (account_id, payment_type, btc_address, btc_amount, usd_amount, status, expires_at)
|
||||
VALUES ($1, 'bitcoin', $2, $3, $4, 'pending', $5)
|
||||
RETURNING id, account_id, payment_type, btc_address, btc_amount, usd_amount, confirmations, status, expires_at, created_at`,
|
||||
accountID, btcAddress, btcAmount, usdAmount, expiresAt).Scan(
|
||||
&payment.ID, &payment.AccountID, &payment.PaymentType, &payment.BTCAddress,
|
||||
&payment.BTCAmount, &payment.USDAmount, &payment.Confirmations, &payment.Status, &payment.CreatedAt)
|
||||
&payment.BTCAmount, &payment.USDAmount, &payment.Confirmations, &payment.Status, &payment.ExpiresAt, &payment.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create payment: %w", err)
|
||||
}
|
||||
@ -221,7 +229,6 @@ func (s *AccountService) ProcessMonthlyBilling(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
lastMonth := now.AddDate(0, -1, 0)
|
||||
billingStart := time.Date(lastMonth.Year(), lastMonth.Month(), 1, 0, 0, 0, 0, lastMonth.Location())
|
||||
billingEnd := billingStart.AddDate(0, 1, 0).Add(-time.Second)
|
||||
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT account_id, max_storage_bytes
|
||||
@ -282,9 +289,137 @@ func (s *AccountService) chargeAccount(ctx context.Context, accountID int64, amo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AccountService) getBTCPrice() (float64, error) {
|
||||
func (s *AccountService) getBTCPrice(ctx context.Context) (float64, error) {
|
||||
var rate float64
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT usd_rate FROM crypto_rates WHERE currency = 'BTC'
|
||||
AND updated_at > NOW() - INTERVAL '1 hour'`).Scan(&rate)
|
||||
|
||||
if err != nil {
|
||||
rate = 45000.0
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
INSERT INTO crypto_rates (currency, usd_rate) VALUES ('BTC', $1)
|
||||
ON CONFLICT (currency) DO UPDATE SET usd_rate = $1, updated_at = NOW()`,
|
||||
rate)
|
||||
if err != nil {
|
||||
return 45000.0, nil
|
||||
}
|
||||
}
|
||||
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
func (s *AccountService) CheckPaymentStatus(ctx context.Context, paymentID int64) (*Payment, error) {
|
||||
var payment Payment
|
||||
var expiresAt *time.Time
|
||||
var confirmedAt *time.Time
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, account_id, payment_type, btc_address, btc_amount, usd_amount,
|
||||
confirmations, tx_hash, status, expires_at, created_at, confirmed_at
|
||||
FROM payments WHERE id = $1`, paymentID).Scan(
|
||||
&payment.ID, &payment.AccountID, &payment.PaymentType, &payment.BTCAddress,
|
||||
&payment.BTCAmount, &payment.USDAmount, &payment.Confirmations, &payment.TxHash,
|
||||
&payment.Status, &expiresAt, &payment.CreatedAt, &confirmedAt)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payment not found: %w", err)
|
||||
}
|
||||
|
||||
payment.ExpiresAt = expiresAt
|
||||
payment.ConfirmedAt = confirmedAt
|
||||
|
||||
if payment.Status == "pending" && expiresAt != nil && time.Now().After(*expiresAt) {
|
||||
_, err = s.pool.Exec(ctx, `UPDATE payments SET status = 'expired' WHERE id = $1`, paymentID)
|
||||
if err == nil {
|
||||
payment.Status = "expired"
|
||||
}
|
||||
}
|
||||
|
||||
return &payment, nil
|
||||
}
|
||||
|
||||
func (s *AccountService) GetAccountPayments(ctx context.Context, accountID int64) ([]Payment, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, account_id, payment_type, btc_address, btc_amount, usd_amount,
|
||||
confirmations, tx_hash, status, expires_at, created_at, confirmed_at
|
||||
FROM payments WHERE account_id = $1 ORDER BY created_at DESC`, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var payments []Payment
|
||||
for rows.Next() {
|
||||
var payment Payment
|
||||
var expiresAt *time.Time
|
||||
var confirmedAt *time.Time
|
||||
|
||||
err := rows.Scan(&payment.ID, &payment.AccountID, &payment.PaymentType,
|
||||
&payment.BTCAddress, &payment.BTCAmount, &payment.USDAmount,
|
||||
&payment.Confirmations, &payment.TxHash, &payment.Status,
|
||||
&expiresAt, &payment.CreatedAt, &confirmedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
payment.ExpiresAt = expiresAt
|
||||
payment.ConfirmedAt = confirmedAt
|
||||
payments = append(payments, payment)
|
||||
}
|
||||
|
||||
return payments, nil
|
||||
}
|
||||
|
||||
func (s *AccountService) GetUsageRecords(ctx context.Context, accountID int64) ([]UsageRecord, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, account_id, billing_period_start, billing_period_end,
|
||||
max_storage_bytes, charge_usd, charged_at, created_at
|
||||
FROM usage_records WHERE account_id = $1 ORDER BY billing_period_start DESC`, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []UsageRecord
|
||||
for rows.Next() {
|
||||
var record UsageRecord
|
||||
var chargedAt *time.Time
|
||||
|
||||
err := rows.Scan(&record.ID, &record.AccountID, &record.BillingPeriodStart,
|
||||
&record.BillingPeriodEnd, &record.MaxStorageBytes, &record.ChargeUSD,
|
||||
&chargedAt, &record.CreatedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
record.ChargedAt = chargedAt
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (s *AccountService) CheckResourceLimits(ctx context.Context, accountID int64, requestedBytes int64) error {
|
||||
var balance float64
|
||||
err := s.pool.QueryRow(ctx, `SELECT balance_usd FROM accounts WHERE id = $1`, accountID).Scan(&balance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("account not found: %w", err)
|
||||
}
|
||||
|
||||
if balance <= 0 {
|
||||
return fmt.Errorf("insufficient account balance")
|
||||
}
|
||||
|
||||
maxBytesAffordable := int64(balance / s.pricePerGB * 1024 * 1024 * 1024)
|
||||
|
||||
if requestedBytes > maxBytesAffordable {
|
||||
return fmt.Errorf("requested storage (%d bytes) exceeds affordable limit (%d bytes)",
|
||||
requestedBytes, maxBytesAffordable)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AccountService) generateBTCAddress() string {
|
||||
bytes := make([]byte, 20)
|
||||
|
||||
@ -19,10 +19,18 @@ CREATE TABLE IF NOT EXISTS payments (
|
||||
confirmations INT DEFAULT 0,
|
||||
tx_hash TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
expires_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
confirmed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_rates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
currency VARCHAR(10) NOT NULL UNIQUE,
|
||||
usd_rate DECIMAL(18,8) NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage_records (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
|
||||
@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -25,8 +26,9 @@ func NewHandlers(bucketService *db.BucketService, accountService *db.AccountServ
|
||||
|
||||
func (h *Handlers) RootHandler(c echo.Context) error {
|
||||
return c.JSON(200, map[string]string{
|
||||
"status": "😺",
|
||||
"docs": "https://illfillthisoutlater.com",
|
||||
"service": "TermCloud Storage API",
|
||||
"version": "1.0.0",
|
||||
"docs": "https://github.com/termcloud/termcloud",
|
||||
})
|
||||
}
|
||||
|
||||
@ -51,21 +53,92 @@ func (h *Handlers) AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) CreateBucketHandler(c echo.Context) error {
|
||||
func (h *Handlers) CreateAccountHandler(c echo.Context) error {
|
||||
account, err := h.accountService.CreateAccount(context.Background())
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to create account"})
|
||||
}
|
||||
|
||||
return c.JSON(201, account)
|
||||
}
|
||||
|
||||
func (h *Handlers) GetAccountHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
return c.JSON(200, account)
|
||||
}
|
||||
|
||||
func (h *Handlers) CreatePaymentHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
USDAmount float64 `json:"usd_amount"`
|
||||
}
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(400, map[string]string{"error": "Invalid request body"})
|
||||
if err := c.Bind(&req); err != nil || req.USDAmount < 5.0 {
|
||||
return c.JSON(400, map[string]string{"error": "Minimum payment amount is $5.00 USD"})
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
payment, err := h.accountService.CreatePayment(context.Background(), account.ID, req.USDAmount)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to create payment"})
|
||||
}
|
||||
|
||||
return c.JSON(201, payment)
|
||||
}
|
||||
|
||||
func (h *Handlers) GetPaymentHandler(c echo.Context) error {
|
||||
paymentID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(400, map[string]string{"error": "Invalid payment ID"})
|
||||
}
|
||||
|
||||
payment, err := h.accountService.CheckPaymentStatus(context.Background(), paymentID)
|
||||
if err != nil {
|
||||
return c.JSON(404, map[string]string{"error": "Payment not found"})
|
||||
}
|
||||
|
||||
return c.JSON(200, payment)
|
||||
}
|
||||
|
||||
func (h *Handlers) GetPaymentsHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
|
||||
payments, err := h.accountService.GetAccountPayments(context.Background(), account.ID)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to get payments"})
|
||||
}
|
||||
|
||||
return c.JSON(200, payments)
|
||||
}
|
||||
|
||||
func (h *Handlers) ConfirmPaymentHandler(c echo.Context) error {
|
||||
paymentID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(400, map[string]string{"error": "Invalid payment ID"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TxHash string `json:"tx_hash"`
|
||||
}
|
||||
if err := c.Bind(&req); err != nil || req.TxHash == "" {
|
||||
return c.JSON(400, map[string]string{"error": "Transaction hash required"})
|
||||
}
|
||||
|
||||
if err := h.accountService.ConfirmPayment(context.Background(), paymentID, req.TxHash); err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to confirm payment"})
|
||||
}
|
||||
|
||||
return c.JSON(200, map[string]string{"message": "Payment confirmed successfully"})
|
||||
}
|
||||
|
||||
func (h *Handlers) CreateBucketHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
if bucketName == "" {
|
||||
return c.JSON(400, map[string]string{"error": "Bucket name is required"})
|
||||
}
|
||||
|
||||
bucket, err := h.bucketService.CreateBucket(context.Background(), req.Name, account.ID)
|
||||
bucket, err := h.bucketService.CreateBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate key") {
|
||||
return c.JSON(409, map[string]string{"error": "Bucket name already exists"})
|
||||
@ -75,24 +148,38 @@ func (h *Handlers) CreateBucketHandler(c echo.Context) error {
|
||||
|
||||
return c.JSON(201, bucket)
|
||||
}
|
||||
func (h *Handlers) ListBucketsHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
|
||||
buckets, err := h.bucketService.GetUserBuckets(context.Background(), user.ID)
|
||||
func (h *Handlers) ListBucketsHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
|
||||
buckets, err := h.bucketService.GetUserBuckets(context.Background(), account.ID)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to list buckets"})
|
||||
}
|
||||
|
||||
return c.JSON(200, map[string]any{
|
||||
"buckets": buckets,
|
||||
})
|
||||
return c.JSON(200, buckets)
|
||||
}
|
||||
|
||||
func (h *Handlers) GetBucketHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
}
|
||||
return c.JSON(500, map[string]string{"error": "Failed to get bucket"})
|
||||
}
|
||||
|
||||
return c.JSON(200, bucket)
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteBucketHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
if err := h.bucketService.DeleteBucket(context.Background(), bucketName, user.ID); err != nil {
|
||||
if err := h.bucketService.DeleteBucket(context.Background(), bucketName, account.ID); err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
}
|
||||
@ -102,12 +189,12 @@ func (h *Handlers) DeleteBucketHandler(c echo.Context) error {
|
||||
return c.JSON(200, map[string]string{"message": "Bucket deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *Handlers) UploadObjectHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
func (h *Handlers) PutObjectHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
objectKey := c.Param("*")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, user.ID)
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
@ -115,61 +202,41 @@ func (h *Handlers) UploadObjectHandler(c echo.Context) error {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to get bucket"})
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.JSON(400, map[string]string{"error": "Failed to retrieve file"})
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to open file"})
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
contentType := c.Request().Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
object, err := h.bucketService.UploadObject(context.Background(), bucket.ID, objectKey, file.Size, contentType, src)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "storage limit exceeded") {
|
||||
return c.JSON(413, map[string]string{"error": "Storage limit exceeded"})
|
||||
contentLength := c.Request().ContentLength
|
||||
if contentLength <= 0 {
|
||||
return c.JSON(400, map[string]string{"error": "Content-Length header required"})
|
||||
}
|
||||
|
||||
if err := h.accountService.CheckResourceLimits(context.Background(), account.ID, contentLength); err != nil {
|
||||
return c.JSON(413, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
body := c.Request().Body
|
||||
defer body.Close()
|
||||
|
||||
object, err := h.bucketService.UploadObject(context.Background(), bucket.ID, objectKey, contentLength, contentType, body)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to upload object"})
|
||||
}
|
||||
|
||||
if err := h.accountService.RecordUsage(context.Background(), account.ID, bucket.StorageUsedBytes+contentLength); err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to record usage"})
|
||||
}
|
||||
|
||||
return c.JSON(201, object)
|
||||
}
|
||||
|
||||
func (h *Handlers) ListObjectsHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, user.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
}
|
||||
return c.JSON(500, map[string]string{"error": "Failed to get bucket"})
|
||||
}
|
||||
|
||||
objects, err := h.bucketService.ListObjects(context.Background(), bucket.ID)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to list objects"})
|
||||
}
|
||||
|
||||
return c.JSON(200, map[string]any{
|
||||
"objects": objects,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handlers) GetObjectHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
objectKey := c.Param("*")
|
||||
|
||||
file, err := h.bucketService.GetObjectFile(context.Background(), bucketName, objectKey, user.ID)
|
||||
file, err := h.bucketService.GetObjectFile(context.Background(), bucketName, objectKey, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Object not found"})
|
||||
@ -188,12 +255,33 @@ func (h *Handlers) GetObjectHandler(c echo.Context) error {
|
||||
|
||||
return c.Stream(http.StatusOK, "application/octet-stream", file)
|
||||
}
|
||||
|
||||
func (h *Handlers) ListObjectsHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
}
|
||||
return c.JSON(500, map[string]string{"error": "Failed to get bucket"})
|
||||
}
|
||||
|
||||
objects, err := h.bucketService.ListObjects(context.Background(), bucket.ID)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to list objects"})
|
||||
}
|
||||
|
||||
return c.JSON(200, objects)
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteObjectHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
objectKey := c.Param("*")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, user.ID)
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
@ -208,31 +296,11 @@ func (h *Handlers) DeleteObjectHandler(c echo.Context) error {
|
||||
return c.JSON(200, map[string]string{"message": "Object deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *Handlers) GetUserInfoHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
|
||||
buckets, err := h.bucketService.GetUserBuckets(context.Background(), user.ID)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to get user buckets"})
|
||||
}
|
||||
|
||||
var totalUsage int64
|
||||
for _, bucket := range buckets {
|
||||
totalUsage += bucket.StorageUsedBytes
|
||||
}
|
||||
|
||||
return c.JSON(200, map[string]any{
|
||||
"user": user,
|
||||
"totalUsage": totalUsage,
|
||||
"bucketCount": len(buckets),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handlers) SetBucketPolicyHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, user.ID)
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
@ -240,14 +308,12 @@ func (h *Handlers) SetBucketPolicyHandler(c echo.Context) error {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to get bucket"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Policy string `json:"policy"`
|
||||
}
|
||||
if err := c.Bind(&req); err != nil {
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
return c.JSON(400, map[string]string{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if err := h.bucketService.SetBucketPolicy(context.Background(), bucket.ID, req.Policy); err != nil {
|
||||
if err := h.bucketService.SetBucketPolicy(context.Background(), bucket.ID, string(body)); err != nil {
|
||||
if strings.Contains(err.Error(), "invalid policy") {
|
||||
return c.JSON(400, map[string]string{"error": err.Error()})
|
||||
}
|
||||
@ -258,10 +324,10 @@ func (h *Handlers) SetBucketPolicyHandler(c echo.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handlers) GetBucketPolicyHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, user.ID)
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
@ -281,10 +347,10 @@ func (h *Handlers) GetBucketPolicyHandler(c echo.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteBucketPolicyHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
account := c.Get("account").(*db.Account)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, user.ID)
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, account.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
||||
@ -298,126 +364,3 @@ func (h *Handlers) DeleteBucketPolicyHandler(c echo.Context) error {
|
||||
|
||||
return c.JSON(200, map[string]string{"message": "Bucket policy deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *Handlers) CreateAccountHandler(c echo.Context) error {
|
||||
account, err := h.accountService.CreateAccount(context.Background())
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to create account"})
|
||||
}
|
||||
|
||||
return c.JSON(201, map[string]any{
|
||||
"accountNumber": account.AccountNumber,
|
||||
"accessToken": account.AccessToken,
|
||||
"balanceUsd": account.BalanceUSD,
|
||||
"isActive": account.IsActive,
|
||||
"message": "Account created. Add funds to activate.",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handlers) GetAccountHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
|
||||
return c.JSON(200, map[string]any{
|
||||
"accountNumber": account.AccountNumber,
|
||||
"balanceUsd": account.BalanceUSD,
|
||||
"isActive": account.IsActive,
|
||||
"createdAt": account.CreatedAt,
|
||||
"activatedAt": account.ActivatedAt,
|
||||
"lastBilling": account.LastBillingDate,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handlers) CreatePaymentHandler(c echo.Context) error {
|
||||
account := c.Get("account").(*db.Account)
|
||||
|
||||
var req struct {
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
if err := c.Bind(&req); err != nil || req.Amount < 5.0 {
|
||||
return c.JSON(400, map[string]string{"error": "Minimum payment amount is $5.00"})
|
||||
}
|
||||
|
||||
payment, err := h.accountService.CreatePayment(context.Background(), account.ID, req.Amount)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to create payment"})
|
||||
}
|
||||
|
||||
return c.JSON(201, map[string]any{
|
||||
"paymentId": payment.ID,
|
||||
"btcAddress": payment.BTCAddress,
|
||||
"btcAmount": payment.BTCAmount,
|
||||
"usdAmount": payment.USDAmount,
|
||||
"status": payment.Status,
|
||||
"message": "Send exact BTC amount to the address above",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handlers) ConfirmPaymentHandler(c echo.Context) error {
|
||||
paymentID, err := strconv.ParseInt(c.Param("paymentId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(400, map[string]string{"error": "Invalid payment ID"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TxHash string `json:"txHash"`
|
||||
}
|
||||
if err := c.Bind(&req); err != nil || req.TxHash == "" {
|
||||
return c.JSON(400, map[string]string{"error": "Transaction hash required"})
|
||||
}
|
||||
|
||||
if err := h.accountService.ConfirmPayment(context.Background(), paymentID, req.TxHash); err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Failed to confirm payment"})
|
||||
}
|
||||
|
||||
return c.JSON(200, map[string]string{"message": "Payment confirmed and account credited"})
|
||||
}
|
||||
|
||||
func (h *Handlers) PolicyEnforcementMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
user := c.Get("user").(*db.User)
|
||||
bucketName := c.Param("bucket")
|
||||
|
||||
if bucketName == "" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
bucket, err := h.bucketService.GetBucket(context.Background(), bucketName, user.ID)
|
||||
if err != nil {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
action := mapHTTPMethodToAction(c.Request().Method, c.Path())
|
||||
resource := fmt.Sprintf("arn:termcloud:s3:::%s/*", bucketName)
|
||||
principal := user.Username
|
||||
|
||||
allowed, err := h.bucketService.EvaluatePolicy(context.Background(), bucket.ID, action, resource, principal)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": "Policy evaluation failed"})
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
return c.JSON(403, map[string]string{"error": "Access denied by bucket policy"})
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func mapHTTPMethodToAction(method, path string) string {
|
||||
switch method {
|
||||
case "GET":
|
||||
if strings.Contains(path, "/objects") && !strings.HasSuffix(path, "/objects") {
|
||||
return "termcloud:GetObject"
|
||||
}
|
||||
return "termcloud:ListObjects"
|
||||
case "PUT":
|
||||
return "termcloud:PutObject"
|
||||
case "DELETE":
|
||||
if strings.Contains(path, "/objects") {
|
||||
return "termcloud:DeleteObject"
|
||||
}
|
||||
return "termcloud:DeleteBucket"
|
||||
default:
|
||||
return "termcloud:GetBucket"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user