Compare commits
2 Commits
3be0ede054
...
efd008f12a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efd008f12a | ||
|
|
46e3901470 |
384
README.md
384
README.md
@ -1,130 +1,221 @@
|
|||||||
# 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
|
### 1. Set up the Server
|
||||||
- **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)
|
```bash
|
||||||
- **Bucket Policies**: AWS S3-compatible JSON policies for access control
|
git clone https://git.keircn.com/keiran/termcloud
|
||||||
- **RESTful API**: Complete REST API for all operations
|
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
|
## Authentication System
|
||||||
|
|
||||||
Similar to Mullvad VPN:
|
|
||||||
1. **Create Account**: Generate anonymous 16-digit account number + access token
|
1. **Create Account**: Generate anonymous 16-digit account number + access token
|
||||||
2. **Add Funds**: Pay ~$5 worth of Bitcoin to activate account
|
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)
|
3. **Usage Billing**: Charged monthly for peak storage usage ($0.50/GB default)
|
||||||
4. **No Personal Info**: No emails, usernames, or personal information required
|
4. **No Personal Info**: No emails, usernames, or personal information required
|
||||||
|
|
||||||
## Setup
|
## Server Configuration
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
| ------------------ | ------- | ------------------------------------- |
|
||||||
| `PRICE_PER_GB_USD` | 0.50 | Monthly charge per GB of peak storage |
|
| `PRICE_PER_GB_USD` | 0.50 | Monthly charge per GB of peak storage |
|
||||||
| `DATABASE_URL` | - | PostgreSQL connection string |
|
| `DATABASE_URL` | - | PostgreSQL connection string |
|
||||||
| `PORT` | 8080 | Server port |
|
| `PORT` | 8080 | Server port |
|
||||||
| `STORAGE_DIR` | storage | Directory for file storage |
|
| `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
|
```bash
|
||||||
make build
|
# Create account (public endpoint)
|
||||||
make run
|
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
|
```bash
|
||||||
curl -X POST http://localhost:8080/api/v1/accounts
|
# List buckets
|
||||||
```
|
GET /api/buckets
|
||||||
Response:
|
Headers: X-Access-Token: your-token
|
||||||
```json
|
|
||||||
{
|
# Create bucket
|
||||||
"accountNumber": "1234567890123456",
|
PUT /api/buckets/my-bucket
|
||||||
"accessToken": "abc123...",
|
Headers: X-Access-Token: your-token
|
||||||
"balanceUsd": 0.00,
|
|
||||||
"isActive": false,
|
# Get bucket info
|
||||||
"message": "Account created. Add funds to activate."
|
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
|
```bash
|
||||||
curl -X POST http://localhost:8080/api/v1/account/payments \
|
# Upload file
|
||||||
-H "X-Access-Token: your-access-token" \
|
PUT /api/buckets/my-bucket/objects/file.txt
|
||||||
-H "Content-Type: application/json" \
|
Headers: X-Access-Token: your-token
|
||||||
-d '{"amount": 5.00}'
|
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)
|
## Bucket Policies (S3-Compatible)
|
||||||
All storage endpoints require `X-Access-Token` header:
|
|
||||||
|
|
||||||
### 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
|
```json
|
||||||
{
|
{
|
||||||
"Version": "2012-10-17",
|
"Version": "2012-10-17",
|
||||||
"Statement": [
|
"Statement": [
|
||||||
{
|
{
|
||||||
"Sid": "StatementId",
|
"Effect": "Allow",
|
||||||
"Effect": "Allow|Deny",
|
"Principal": { "User": ["john"] },
|
||||||
"Principal": {
|
"Action": ["termcloud:GetObject", "termcloud:ListObjects"],
|
||||||
"User": ["username1", "username2"]
|
"Resource": ["arn:termcloud:s3:::my-bucket/*"]
|
||||||
},
|
|
||||||
"Action": [
|
|
||||||
"termcloud:GetObject",
|
|
||||||
"termcloud:PutObject",
|
|
||||||
"termcloud:DeleteObject",
|
|
||||||
"termcloud:ListObjects"
|
|
||||||
],
|
|
||||||
"Resource": [
|
|
||||||
"arn:termcloud:s3:::bucket-name/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -135,59 +226,68 @@ Bucket policies use JSON format similar to AWS S3 IAM policies to control access
|
|||||||
- `termcloud:GetObject` - Download files
|
- `termcloud:GetObject` - Download files
|
||||||
- `termcloud:PutObject` - Upload files
|
- `termcloud:PutObject` - Upload files
|
||||||
- `termcloud:DeleteObject` - Delete files
|
- `termcloud:DeleteObject` - Delete files
|
||||||
- `termcloud:ListObjects` - List files in bucket
|
- `termcloud:ListObjects` - List files
|
||||||
- `termcloud:GetBucket` - Get bucket info
|
- `termcloud:GetBucket` - Get bucket info
|
||||||
- `termcloud:DeleteBucket` - Delete bucket
|
- `termcloud:DeleteBucket` - Delete bucket
|
||||||
- `*` - All actions
|
|
||||||
|
|
||||||
### Policy Examples
|
### Policy Management
|
||||||
|
|
||||||
**Read-only access:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Principal": {"User": ["john"]},
|
|
||||||
"Action": ["termcloud:GetObject", "termcloud:ListObjects"],
|
|
||||||
"Resource": ["arn:termcloud:s3:::my-bucket/*"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deny delete operations:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Effect": "Deny",
|
|
||||||
"Principal": {"User": ["*"]},
|
|
||||||
"Action": ["termcloud:DeleteObject"],
|
|
||||||
"Resource": ["arn:termcloud:s3:::my-bucket/*"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create bucket
|
# Set bucket policy
|
||||||
curl -X POST http://localhost:8080/api/v1/buckets \
|
PUT /api/buckets/my-bucket/policy
|
||||||
-H "X-API-Key: your-api-key" \
|
Headers: X-Access-Token: your-token
|
||||||
-H "Content-Type: application/json" \
|
Body: [JSON policy]
|
||||||
-d '{"name": "my-files"}'
|
|
||||||
|
|
||||||
# Upload file
|
# Get bucket policy
|
||||||
curl -X PUT http://localhost:8080/api/v1/buckets/my-files/objects/test.txt \
|
GET /api/buckets/my-bucket/policy
|
||||||
-H "X-API-Key: your-api-key" \
|
Headers: X-Access-Token: your-token
|
||||||
-F "file=@test.txt"
|
|
||||||
|
|
||||||
# Download file
|
# Delete bucket policy
|
||||||
curl http://localhost:8080/api/v1/buckets/my-files/objects/test.txt \
|
DELETE /api/buckets/my-bucket/policy
|
||||||
-H "X-API-Key: your-api-key" \
|
Headers: X-Access-Token: your-token
|
||||||
-o downloaded.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|||||||
27
cmd/tcman/main.go
Normal file
27
cmd/tcman/main.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.keircn.com/keiran/termcloud/internal/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
|
Use: "tcman",
|
||||||
|
Short: "TermCloud Management CLI",
|
||||||
|
Long: "Command line interface for managing TermCloud accounts, buckets, and files",
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.AddCommand(cli.NewAccountCmd())
|
||||||
|
rootCmd.AddCommand(cli.NewBucketCmd())
|
||||||
|
rootCmd.AddCommand(cli.NewFileCmd())
|
||||||
|
rootCmd.AddCommand(cli.NewConfigCmd())
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -44,17 +44,20 @@ func main() {
|
|||||||
|
|
||||||
e.GET("/", h.RootHandler)
|
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.Use(h.AuthMiddleware)
|
||||||
|
|
||||||
api.GET("/account", h.GetAccountHandler)
|
api.GET("/account", h.GetAccountHandler)
|
||||||
api.POST("/account/payments", h.CreatePaymentHandler)
|
api.POST("/payments", h.CreatePaymentHandler)
|
||||||
api.POST("/account/payments/:paymentId/confirm", h.ConfirmPaymentHandler)
|
api.GET("/payments", h.GetPaymentsHandler)
|
||||||
|
api.GET("/payments/:id", h.GetPaymentHandler)
|
||||||
|
api.POST("/payments/:id/confirm", h.ConfirmPaymentHandler)
|
||||||
|
|
||||||
api.GET("/buckets", h.ListBucketsHandler)
|
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.DELETE("/buckets/:bucket", h.DeleteBucketHandler)
|
||||||
|
|
||||||
api.PUT("/buckets/:bucket/policy", h.SetBucketPolicyHandler)
|
api.PUT("/buckets/:bucket/policy", h.SetBucketPolicyHandler)
|
||||||
@ -62,7 +65,7 @@ func main() {
|
|||||||
api.DELETE("/buckets/:bucket/policy", h.DeleteBucketPolicyHandler)
|
api.DELETE("/buckets/:bucket/policy", h.DeleteBucketPolicyHandler)
|
||||||
|
|
||||||
api.GET("/buckets/:bucket/objects", h.ListObjectsHandler)
|
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.GET("/buckets/:bucket/objects/*", h.GetObjectHandler)
|
||||||
api.DELETE("/buckets/:bucket/objects/*", h.DeleteObjectHandler)
|
api.DELETE("/buckets/:bucket/objects/*", h.DeleteObjectHandler)
|
||||||
|
|
||||||
|
|||||||
3
go.mod
3
go.mod
@ -9,12 +9,15 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/spf13/cobra v1.9.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
golang.org/x/crypto v0.38.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
|||||||
8
go.sum
8
go.sum
@ -1,6 +1,9 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@ -19,6 +22,11 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
|||||||
266
internal/cli/account.go
Normal file
266
internal/cli/account.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAccountCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "account",
|
||||||
|
Short: "Manage your TermCloud account",
|
||||||
|
Long: "Create, authenticate, and manage your TermCloud account",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newAccountCreateCmd())
|
||||||
|
cmd.AddCommand(newAccountLoginCmd())
|
||||||
|
cmd.AddCommand(newAccountInfoCmd())
|
||||||
|
cmd.AddCommand(newAccountTopUpCmd())
|
||||||
|
cmd.AddCommand(newAccountPaymentsCmd())
|
||||||
|
cmd.AddCommand(newAccountUsageCmd())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAccountCreateCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a new TermCloud account",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, "")
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
account, err := client.CreateAccount(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AccountNumber = account.AccountNumber
|
||||||
|
if err := config.Save(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Account created successfully!\n")
|
||||||
|
fmt.Printf("Account Number: %s\n", account.AccountNumber)
|
||||||
|
fmt.Printf("Status: %s\n", getAccountStatus(account))
|
||||||
|
fmt.Printf("\nTo activate your account, add funds using: tcman account top-up\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAccountLoginCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "login <account-number> <access-token>",
|
||||||
|
Short: "Login to an existing account",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
accountNumber := args[0]
|
||||||
|
accessToken := args[1]
|
||||||
|
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, accessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
account, err := client.GetAccount(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to authenticate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.AccountNumber != accountNumber {
|
||||||
|
return fmt.Errorf("account number mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AccountNumber = accountNumber
|
||||||
|
config.AccessToken = accessToken
|
||||||
|
if err := config.Save(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully logged in!\n")
|
||||||
|
fmt.Printf("Account: %s\n", account.AccountNumber)
|
||||||
|
fmt.Printf("Status: %s\n", getAccountStatus(account))
|
||||||
|
fmt.Printf("Balance: $%.2f USD\n", account.BalanceUSD)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAccountInfoCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "info",
|
||||||
|
Short: "Show account information",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
account, err := client.GetAccount(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get account info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Account Information:\n")
|
||||||
|
fmt.Printf(" Number: %s\n", account.AccountNumber)
|
||||||
|
fmt.Printf(" Status: %s\n", getAccountStatus(account))
|
||||||
|
fmt.Printf(" Balance: $%.2f USD\n", account.BalanceUSD)
|
||||||
|
fmt.Printf(" Created: %s\n", account.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
if account.ActivatedAt != nil {
|
||||||
|
fmt.Printf(" Activated: %s\n", account.ActivatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
fmt.Printf(" Last Billing: %s\n", account.LastBillingDate.Format("2006-01-02"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAccountTopUpCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "top-up <amount>",
|
||||||
|
Short: "Add funds to your account via Bitcoin",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
amount, err := strconv.ParseFloat(args[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid amount: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount < 5.0 {
|
||||||
|
return fmt.Errorf("minimum top-up amount is $5.00 USD")
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
payment, err := client.CreatePayment(ctx, amount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create payment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Bitcoin Payment Created:\n")
|
||||||
|
fmt.Printf(" Payment ID: %d\n", payment.ID)
|
||||||
|
fmt.Printf(" Amount: $%.2f USD (%.8f BTC)\n", payment.USDAmount, payment.BTCAmount)
|
||||||
|
fmt.Printf(" Address: %s\n", payment.BTCAddress)
|
||||||
|
fmt.Printf(" Status: %s\n", strings.ToUpper(payment.Status))
|
||||||
|
if payment.ExpiresAt != nil {
|
||||||
|
fmt.Printf(" Expires: %s\n", payment.ExpiresAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
fmt.Printf("\nSend exactly %.8f BTC to the address above.\n", payment.BTCAmount)
|
||||||
|
fmt.Printf("Check payment status with: tcman account payments\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAccountPaymentsCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "payments",
|
||||||
|
Short: "List payment history",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
payments, err := client.GetPayments(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get payments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(payments) == 0 {
|
||||||
|
fmt.Printf("No payments found.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "ID\tAmount\tBTC Amount\tStatus\tCreated\tConfirmed")
|
||||||
|
fmt.Fprintln(w, "---\t------\t----------\t------\t-------\t---------")
|
||||||
|
|
||||||
|
for _, payment := range payments {
|
||||||
|
confirmedStr := "N/A"
|
||||||
|
if payment.ConfirmedAt != nil {
|
||||||
|
confirmedStr = payment.ConfirmedAt.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%d\t$%.2f\t%.8f\t%s\t%s\t%s\n",
|
||||||
|
payment.ID,
|
||||||
|
payment.USDAmount,
|
||||||
|
payment.BTCAmount,
|
||||||
|
strings.ToUpper(payment.Status),
|
||||||
|
payment.CreatedAt.Format("2006-01-02 15:04"),
|
||||||
|
confirmedStr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAccountUsageCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "usage",
|
||||||
|
Short: "Show billing and usage history",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Printf("Usage tracking is handled automatically.\n")
|
||||||
|
fmt.Printf("You are charged monthly based on peak storage usage.\n")
|
||||||
|
fmt.Printf("Current rate: $0.50 per GB per month.\n")
|
||||||
|
fmt.Printf("\nUse 'tcman account info' to see your current balance.\n")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAccountStatus(account *Account) string {
|
||||||
|
if !account.IsActive {
|
||||||
|
return "INACTIVE (needs funds)"
|
||||||
|
}
|
||||||
|
if account.BalanceUSD <= 0 {
|
||||||
|
return "LOW BALANCE"
|
||||||
|
}
|
||||||
|
return "ACTIVE"
|
||||||
|
}
|
||||||
221
internal/cli/bucket.go
Normal file
221
internal/cli/bucket.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewBucketCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "bucket",
|
||||||
|
Short: "Manage storage buckets",
|
||||||
|
Long: "Create, list, and manage your storage buckets",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newBucketCreateCmd())
|
||||||
|
cmd.AddCommand(newBucketListCmd())
|
||||||
|
cmd.AddCommand(newBucketDeleteCmd())
|
||||||
|
cmd.AddCommand(newBucketInfoCmd())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBucketCreateCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "create <bucket-name>",
|
||||||
|
Short: "Create a new bucket",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := args[0]
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
bucket, err := client.CreateBucket(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Bucket '%s' created successfully!\n", bucket.Name)
|
||||||
|
fmt.Printf("ID: %d\n", bucket.ID)
|
||||||
|
fmt.Printf("Storage Used: %d bytes\n", bucket.StorageUsedBytes)
|
||||||
|
fmt.Printf("Created: %s\n", bucket.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBucketListCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all buckets",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
buckets, err := client.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list buckets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buckets) == 0 {
|
||||||
|
fmt.Printf("No buckets found.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "Name\tStorage Used\tCreated")
|
||||||
|
fmt.Fprintln(w, "----\t------------\t-------")
|
||||||
|
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
storageStr := formatBytes(bucket.StorageUsedBytes)
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s\n",
|
||||||
|
bucket.Name,
|
||||||
|
storageStr,
|
||||||
|
bucket.CreatedAt.Format("2006-01-02 15:04"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBucketInfoCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "info <bucket-name>",
|
||||||
|
Short: "Show bucket information",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := args[0]
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
bucket, err := client.GetBucket(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get bucket info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Bucket Information:\n")
|
||||||
|
fmt.Printf(" Name: %s\n", bucket.Name)
|
||||||
|
fmt.Printf(" ID: %d\n", bucket.ID)
|
||||||
|
fmt.Printf(" Storage Used: %s\n", formatBytes(bucket.StorageUsedBytes))
|
||||||
|
fmt.Printf(" Created: %s\n", bucket.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
objects, err := client.ListObjects(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" Objects: Error loading (%v)\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Objects: %d\n", len(objects))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBucketDeleteCmd() *cobra.Command {
|
||||||
|
var force bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "delete <bucket-name>",
|
||||||
|
Short: "Delete a bucket",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := args[0]
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
objects, err := client.ListObjects(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check bucket contents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(objects) > 0 {
|
||||||
|
return fmt.Errorf("bucket '%s' contains %d objects. Use --force to delete anyway", bucketName, len(objects))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Are you sure you want to delete bucket '%s'? (y/N): ", bucketName)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "yes" && response != "Y" && response != "YES" {
|
||||||
|
fmt.Printf("Cancelled.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.DeleteBucket(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Bucket '%s' deleted successfully.\n", bucketName)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVar(&force, "force", false, "Force deletion without confirmation")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBytes(bytes int64) string {
|
||||||
|
if bytes == 0 {
|
||||||
|
return "0 B"
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%d B", bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
|
||||||
|
units := []string{"KB", "MB", "GB", "TB"}
|
||||||
|
return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp])
|
||||||
|
}
|
||||||
241
internal/cli/client.go
Normal file
241
internal/cli/client.go
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
AccountNumber string `json:"accountNumber"`
|
||||||
|
BalanceUSD float64 `json:"balanceUsd"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
ActivatedAt *time.Time `json:"activatedAt,omitempty"`
|
||||||
|
LastBillingDate time.Time `json:"lastBillingDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payment struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
AccountID int64 `json:"accountId"`
|
||||||
|
PaymentType string `json:"paymentType"`
|
||||||
|
BTCAddress string `json:"btcAddress,omitempty"`
|
||||||
|
BTCAmount float64 `json:"btcAmount,omitempty"`
|
||||||
|
USDAmount float64 `json:"usdAmount"`
|
||||||
|
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 Bucket struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
OwnerID int64 `json:"ownerId"`
|
||||||
|
StorageUsedBytes int64 `json:"storageUsedBytes"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Object struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
BucketID int64 `json:"bucketId"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
SizeBytes int64 `json:"sizeBytes"`
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
LastModified time.Time `json:"lastModified"`
|
||||||
|
VersionID string `json:"versionId"`
|
||||||
|
MD5Checksum string `json:"md5Checksum"`
|
||||||
|
CustomMetadata map[string]interface{} `json:"customMetadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(baseURL, token string) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: baseURL,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
jsonData, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewBuffer(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("X-Access-Token", c.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("server error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != nil && resp.StatusCode != http.StatusNoContent {
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateAccount(ctx context.Context) (*Account, error) {
|
||||||
|
var account Account
|
||||||
|
err := c.makeRequest(ctx, "POST", "/api/accounts", nil, &account)
|
||||||
|
return &account, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetAccount(ctx context.Context) (*Account, error) {
|
||||||
|
var account Account
|
||||||
|
err := c.makeRequest(ctx, "GET", "/api/account", nil, &account)
|
||||||
|
return &account, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreatePayment(ctx context.Context, usdAmount float64) (*Payment, error) {
|
||||||
|
req := map[string]interface{}{
|
||||||
|
"usd_amount": usdAmount,
|
||||||
|
}
|
||||||
|
var payment Payment
|
||||||
|
err := c.makeRequest(ctx, "POST", "/api/payments", req, &payment)
|
||||||
|
return &payment, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetPayment(ctx context.Context, paymentID int64) (*Payment, error) {
|
||||||
|
var payment Payment
|
||||||
|
path := fmt.Sprintf("/api/payments/%d", paymentID)
|
||||||
|
err := c.makeRequest(ctx, "GET", path, nil, &payment)
|
||||||
|
return &payment, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetPayments(ctx context.Context) ([]Payment, error) {
|
||||||
|
var payments []Payment
|
||||||
|
err := c.makeRequest(ctx, "GET", "/api/payments", nil, &payments)
|
||||||
|
return payments, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateBucket(ctx context.Context, name string) (*Bucket, error) {
|
||||||
|
req := map[string]interface{}{
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
var bucket Bucket
|
||||||
|
err := c.makeRequest(ctx, "PUT", "/api/buckets/"+name, req, &bucket)
|
||||||
|
return &bucket, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListBuckets(ctx context.Context) ([]Bucket, error) {
|
||||||
|
var buckets []Bucket
|
||||||
|
err := c.makeRequest(ctx, "GET", "/api/buckets", nil, &buckets)
|
||||||
|
return buckets, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetBucket(ctx context.Context, name string) (*Bucket, error) {
|
||||||
|
var bucket Bucket
|
||||||
|
path := fmt.Sprintf("/api/buckets/%s", name)
|
||||||
|
err := c.makeRequest(ctx, "GET", path, nil, &bucket)
|
||||||
|
return &bucket, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteBucket(ctx context.Context, name string) error {
|
||||||
|
path := fmt.Sprintf("/api/buckets/%s", name)
|
||||||
|
return c.makeRequest(ctx, "DELETE", path, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListObjects(ctx context.Context, bucketName string) ([]Object, error) {
|
||||||
|
var objects []Object
|
||||||
|
path := fmt.Sprintf("/api/buckets/%s/objects", bucketName)
|
||||||
|
err := c.makeRequest(ctx, "GET", path, nil, &objects)
|
||||||
|
return objects, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PutObject(ctx context.Context, bucketName, key string, data []byte, contentType string) error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "PUT",
|
||||||
|
fmt.Sprintf("%s/api/buckets/%s/objects/%s", c.baseURL, bucketName, key),
|
||||||
|
bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType != "" {
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("X-Access-Token", c.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("upload failed %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetObject(ctx context.Context, bucketName, key string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET",
|
||||||
|
fmt.Sprintf("%s/api/buckets/%s/objects/%s", c.baseURL, bucketName, key), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("X-Access-Token", c.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("download failed %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteObject(ctx context.Context, bucketName, key string) error {
|
||||||
|
path := fmt.Sprintf("/api/buckets/%s/objects/%s", bucketName, key)
|
||||||
|
return c.makeRequest(ctx, "DELETE", path, nil, nil)
|
||||||
|
}
|
||||||
132
internal/cli/cmd_config.go
Normal file
132
internal/cli/cmd_config.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConfigCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Manage CLI configuration",
|
||||||
|
Long: "View and modify CLI configuration settings",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newConfigShowCmd())
|
||||||
|
cmd.AddCommand(newConfigSetCmd())
|
||||||
|
cmd.AddCommand(newConfigResetCmd())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigShowCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "show",
|
||||||
|
Short: "Show current configuration",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, _ := GetConfigPath()
|
||||||
|
|
||||||
|
fmt.Printf("Configuration:\n")
|
||||||
|
fmt.Printf(" Config file: %s\n", configPath)
|
||||||
|
fmt.Printf(" Server URL: %s\n", config.ServerURL)
|
||||||
|
fmt.Printf(" Account Number: %s\n", getConfigValue(config.AccountNumber, "Not set"))
|
||||||
|
fmt.Printf(" Access Token: %s\n", getConfigValue(config.AccessToken, "Not set"))
|
||||||
|
|
||||||
|
if config.IsAuthenticated() {
|
||||||
|
fmt.Printf(" Status: Authenticated\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Status: Not authenticated\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigSetCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "set <key> <value>",
|
||||||
|
Short: "Set a configuration value",
|
||||||
|
Long: `Set a configuration value. Available keys:
|
||||||
|
server-url - TermCloud server URL (e.g., https://api.termcloud.com)`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
key := args[0]
|
||||||
|
value := args[1]
|
||||||
|
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "server-url":
|
||||||
|
config.ServerURL = value
|
||||||
|
fmt.Printf("Server URL set to: %s\n", value)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown config key: %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Save(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigResetCmd() *cobra.Command {
|
||||||
|
var force bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "reset",
|
||||||
|
Short: "Reset configuration to defaults",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if !force {
|
||||||
|
fmt.Printf("This will delete all configuration including login credentials.\n")
|
||||||
|
fmt.Printf("Are you sure? (y/N): ")
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "yes" && response != "Y" && response != "YES" {
|
||||||
|
fmt.Printf("Cancelled.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to remove config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Configuration reset successfully.\n")
|
||||||
|
fmt.Printf("You'll need to log in again with 'tcman account login' or create a new account.\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVar(&force, "force", false, "Force reset without confirmation")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigValue(value, defaultValue string) string {
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
if len(value) > 20 {
|
||||||
|
return value[:17] + "..."
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
83
internal/cli/config.go
Normal file
83
internal/cli/config.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ServerURL string `json:"serverUrl"`
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
AccountNumber string `json:"accountNumber"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigDir() (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Join(home, ".tcman")
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return configDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigPath() (string, error) {
|
||||||
|
configDir, err := GetConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(configDir, "config.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
configPath, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &Config{
|
||||||
|
ServerURL: "http://localhost:8080",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ServerURL == "" {
|
||||||
|
config.ServerURL = "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Save() error {
|
||||||
|
configPath, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(c, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(configPath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) IsAuthenticated() bool {
|
||||||
|
return c.AccessToken != "" && c.AccountNumber != ""
|
||||||
|
}
|
||||||
235
internal/cli/file.go
Normal file
235
internal/cli/file.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFileCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "file",
|
||||||
|
Short: "Manage files in buckets",
|
||||||
|
Long: "Upload, download, list, and manage files in your buckets",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newFileUploadCmd())
|
||||||
|
cmd.AddCommand(newFileDownloadCmd())
|
||||||
|
cmd.AddCommand(newFileListCmd())
|
||||||
|
cmd.AddCommand(newFileDeleteCmd())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileUploadCmd() *cobra.Command {
|
||||||
|
var contentType string
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "upload <bucket-name> <local-file> [remote-key]",
|
||||||
|
Short: "Upload a file to a bucket",
|
||||||
|
Args: cobra.RangeArgs(2, 3),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := args[0]
|
||||||
|
localFile := args[1]
|
||||||
|
|
||||||
|
remoteKey := filepath.Base(localFile)
|
||||||
|
if len(args) == 3 {
|
||||||
|
remoteKey = args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(localFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = mime.TypeByExtension(filepath.Ext(localFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fmt.Printf("Uploading %s to %s/%s (%s)...\n", localFile, bucketName, remoteKey, formatBytes(int64(len(data))))
|
||||||
|
|
||||||
|
err = client.PutObject(ctx, bucketName, remoteKey, data, contentType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("File uploaded successfully!\n")
|
||||||
|
fmt.Printf("Bucket: %s\n", bucketName)
|
||||||
|
fmt.Printf("Key: %s\n", remoteKey)
|
||||||
|
fmt.Printf("Size: %s\n", formatBytes(int64(len(data))))
|
||||||
|
if contentType != "" {
|
||||||
|
fmt.Printf("Content-Type: %s\n", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&contentType, "content-type", "", "Override content type")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileDownloadCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "download <bucket-name> <remote-key> [local-file]",
|
||||||
|
Short: "Download a file from a bucket",
|
||||||
|
Args: cobra.RangeArgs(2, 3),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := args[0]
|
||||||
|
remoteKey := args[1]
|
||||||
|
|
||||||
|
localFile := filepath.Base(remoteKey)
|
||||||
|
if len(args) == 3 {
|
||||||
|
localFile = args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fmt.Printf("Downloading %s/%s to %s...\n", bucketName, remoteKey, localFile)
|
||||||
|
|
||||||
|
data, err := client.GetObject(ctx, bucketName, remoteKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(localFile, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("File downloaded successfully!\n")
|
||||||
|
fmt.Printf("Size: %s\n", formatBytes(int64(len(data))))
|
||||||
|
fmt.Printf("Saved to: %s\n", localFile)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileListCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list <bucket-name>",
|
||||||
|
Short: "List files in a bucket",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := args[0]
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
objects, err := client.ListObjects(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list objects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(objects) == 0 {
|
||||||
|
fmt.Printf("No files found in bucket '%s'.\n", bucketName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "Key\tSize\tContent-Type\tLast Modified")
|
||||||
|
fmt.Fprintln(w, "---\t----\t------------\t-------------")
|
||||||
|
|
||||||
|
for _, obj := range objects {
|
||||||
|
contentType := obj.ContentType
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
|
||||||
|
obj.Key,
|
||||||
|
formatBytes(obj.SizeBytes),
|
||||||
|
contentType,
|
||||||
|
obj.LastModified.Format("2006-01-02 15:04:05"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileDeleteCmd() *cobra.Command {
|
||||||
|
var force bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "delete <bucket-name> <remote-key>",
|
||||||
|
Short: "Delete a file from a bucket",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsAuthenticated() {
|
||||||
|
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := args[0]
|
||||||
|
remoteKey := args[1]
|
||||||
|
client := NewClient(config.ServerURL, config.AccessToken)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
fmt.Printf("Are you sure you want to delete '%s' from bucket '%s'? (y/N): ", remoteKey, bucketName)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.ToLower(response)
|
||||||
|
if response != "y" && response != "yes" {
|
||||||
|
fmt.Printf("Cancelled.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.DeleteObject(ctx, bucketName, remoteKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("File '%s' deleted from bucket '%s'.\n", remoteKey, bucketName)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVar(&force, "force", false, "Force deletion without confirmation")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@ -32,10 +32,17 @@ type Payment struct {
|
|||||||
Confirmations int `json:"confirmations"`
|
Confirmations int `json:"confirmations"`
|
||||||
TxHash string `json:"txHash,omitempty"`
|
TxHash string `json:"txHash,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
ConfirmedAt *time.Time `json:"confirmedAt,omitempty"`
|
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 {
|
type UsageRecord struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
AccountID int64 `json:"accountId"`
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get BTC price: %w", err)
|
return nil, fmt.Errorf("failed to get BTC price: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
btcAmount := usdAmount / btcPrice
|
btcAmount := usdAmount / btcPrice
|
||||||
btcAddress := s.generateBTCAddress()
|
btcAddress := s.generateBTCAddress()
|
||||||
|
expiresAt := time.Now().Add(24 * time.Hour)
|
||||||
|
|
||||||
var payment Payment
|
var payment Payment
|
||||||
err = s.pool.QueryRow(ctx, `
|
err = s.pool.QueryRow(ctx, `
|
||||||
INSERT INTO payments (account_id, payment_type, btc_address, btc_amount, usd_amount, status)
|
INSERT INTO payments (account_id, payment_type, btc_address, btc_amount, usd_amount, status, expires_at)
|
||||||
VALUES ($1, 'bitcoin', $2, $3, $4, 'pending')
|
VALUES ($1, 'bitcoin', $2, $3, $4, 'pending', $5)
|
||||||
RETURNING id, account_id, payment_type, btc_address, btc_amount, usd_amount, confirmations, status, created_at`,
|
RETURNING id, account_id, payment_type, btc_address, btc_amount, usd_amount, confirmations, status, expires_at, created_at`,
|
||||||
accountID, btcAddress, btcAmount, usdAmount).Scan(
|
accountID, btcAddress, btcAmount, usdAmount, expiresAt).Scan(
|
||||||
&payment.ID, &payment.AccountID, &payment.PaymentType, &payment.BTCAddress,
|
&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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create payment: %w", err)
|
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()
|
now := time.Now()
|
||||||
lastMonth := now.AddDate(0, -1, 0)
|
lastMonth := now.AddDate(0, -1, 0)
|
||||||
billingStart := time.Date(lastMonth.Year(), lastMonth.Month(), 1, 0, 0, 0, 0, lastMonth.Location())
|
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, `
|
rows, err := s.pool.Query(ctx, `
|
||||||
SELECT account_id, max_storage_bytes
|
SELECT account_id, max_storage_bytes
|
||||||
@ -282,8 +289,136 @@ func (s *AccountService) chargeAccount(ctx context.Context, accountID int64, amo
|
|||||||
return nil
|
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 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 {
|
func (s *AccountService) generateBTCAddress() string {
|
||||||
|
|||||||
@ -19,10 +19,18 @@ CREATE TABLE IF NOT EXISTS payments (
|
|||||||
confirmations INT DEFAULT 0,
|
confirmations INT DEFAULT 0,
|
||||||
tx_hash TEXT,
|
tx_hash TEXT,
|
||||||
status VARCHAR(20) DEFAULT 'pending',
|
status VARCHAR(20) DEFAULT 'pending',
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
confirmed_at TIMESTAMPTZ
|
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 (
|
CREATE TABLE IF NOT EXISTS usage_records (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -25,8 +26,9 @@ func NewHandlers(bucketService *db.BucketService, accountService *db.AccountServ
|
|||||||
|
|
||||||
func (h *Handlers) RootHandler(c echo.Context) error {
|
func (h *Handlers) RootHandler(c echo.Context) error {
|
||||||
return c.JSON(200, map[string]string{
|
return c.JSON(200, map[string]string{
|
||||||
"status": "😺",
|
"service": "TermCloud Storage API",
|
||||||
"docs": "https://illfillthisoutlater.com",
|
"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)
|
account := c.Get("account").(*db.Account)
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
USDAmount float64 `json:"usd_amount"`
|
||||||
}
|
}
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil || req.USDAmount < 5.0 {
|
||||||
return c.JSON(400, map[string]string{"error": "Invalid request body"})
|
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"})
|
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
return c.JSON(409, map[string]string{"error": "Bucket name already exists"})
|
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)
|
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 {
|
if err != nil {
|
||||||
return c.JSON(500, map[string]string{"error": "Failed to list buckets"})
|
return c.JSON(500, map[string]string{"error": "Failed to list buckets"})
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(200, map[string]any{
|
return c.JSON(200, buckets)
|
||||||
"buckets": 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 {
|
func (h *Handlers) DeleteBucketHandler(c echo.Context) error {
|
||||||
user := c.Get("user").(*db.User)
|
account := c.Get("account").(*db.Account)
|
||||||
bucketName := c.Param("bucket")
|
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") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
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"})
|
return c.JSON(200, map[string]string{"message": "Bucket deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) UploadObjectHandler(c echo.Context) error {
|
func (h *Handlers) PutObjectHandler(c echo.Context) error {
|
||||||
user := c.Get("user").(*db.User)
|
account := c.Get("account").(*db.Account)
|
||||||
bucketName := c.Param("bucket")
|
bucketName := c.Param("bucket")
|
||||||
objectKey := c.Param("*")
|
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
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"})
|
return c.JSON(500, map[string]string{"error": "Failed to get bucket"})
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := c.FormFile("file")
|
contentType := c.Request().Header.Get("Content-Type")
|
||||||
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")
|
|
||||||
if contentType == "" {
|
if contentType == "" {
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
object, err := h.bucketService.UploadObject(context.Background(), bucket.ID, objectKey, file.Size, contentType, src)
|
contentLength := c.Request().ContentLength
|
||||||
if err != nil {
|
if contentLength <= 0 {
|
||||||
if strings.Contains(err.Error(), "storage limit exceeded") {
|
return c.JSON(400, map[string]string{"error": "Content-Length header required"})
|
||||||
return c.JSON(413, map[string]string{"error": "Storage limit exceeded"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"})
|
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)
|
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 {
|
func (h *Handlers) GetObjectHandler(c echo.Context) error {
|
||||||
user := c.Get("user").(*db.User)
|
account := c.Get("account").(*db.Account)
|
||||||
bucketName := c.Param("bucket")
|
bucketName := c.Param("bucket")
|
||||||
objectKey := c.Param("*")
|
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
return c.JSON(404, map[string]string{"error": "Object not found"})
|
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)
|
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 {
|
func (h *Handlers) DeleteObjectHandler(c echo.Context) error {
|
||||||
user := c.Get("user").(*db.User)
|
account := c.Get("account").(*db.Account)
|
||||||
bucketName := c.Param("bucket")
|
bucketName := c.Param("bucket")
|
||||||
objectKey := c.Param("*")
|
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
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"})
|
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 {
|
func (h *Handlers) SetBucketPolicyHandler(c echo.Context) error {
|
||||||
user := c.Get("user").(*db.User)
|
account := c.Get("account").(*db.Account)
|
||||||
bucketName := c.Param("bucket")
|
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
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"})
|
return c.JSON(500, map[string]string{"error": "Failed to get bucket"})
|
||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
body, err := io.ReadAll(c.Request().Body)
|
||||||
Policy string `json:"policy"`
|
if err != nil {
|
||||||
}
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return c.JSON(400, map[string]string{"error": "Invalid request body"})
|
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") {
|
if strings.Contains(err.Error(), "invalid policy") {
|
||||||
return c.JSON(400, map[string]string{"error": err.Error()})
|
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 {
|
func (h *Handlers) GetBucketPolicyHandler(c echo.Context) error {
|
||||||
user := c.Get("user").(*db.User)
|
account := c.Get("account").(*db.Account)
|
||||||
bucketName := c.Param("bucket")
|
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
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 {
|
func (h *Handlers) DeleteBucketPolicyHandler(c echo.Context) error {
|
||||||
user := c.Get("user").(*db.User)
|
account := c.Get("account").(*db.Account)
|
||||||
bucketName := c.Param("bucket")
|
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 err != nil {
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
return c.JSON(404, map[string]string{"error": "Bucket not found"})
|
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"})
|
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