424 lines
13 KiB
Go
424 lines
13 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.keircn.com/keiran/termcloud/internal/db"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
type Handlers struct {
|
|
bucketService *db.BucketService
|
|
accountService *db.AccountService
|
|
}
|
|
|
|
func NewHandlers(bucketService *db.BucketService, accountService *db.AccountService) *Handlers {
|
|
return &Handlers{
|
|
bucketService: bucketService,
|
|
accountService: accountService,
|
|
}
|
|
}
|
|
|
|
func (h *Handlers) RootHandler(c echo.Context) error {
|
|
return c.JSON(200, map[string]string{
|
|
"status": "😺",
|
|
"docs": "https://illfillthisoutlater.com",
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
accessToken := c.Request().Header.Get("X-Access-Token")
|
|
if accessToken == "" {
|
|
return c.JSON(401, map[string]string{"error": "Access token required"})
|
|
}
|
|
|
|
account, err := h.accountService.GetAccountByToken(context.Background(), accessToken)
|
|
if err != nil {
|
|
return c.JSON(401, map[string]string{"error": "Invalid access token"})
|
|
}
|
|
|
|
if !account.IsActive {
|
|
return c.JSON(403, map[string]string{"error": "Account inactive - please add funds"})
|
|
}
|
|
|
|
c.Set("account", account)
|
|
return next(c)
|
|
}
|
|
}
|
|
|
|
func (h *Handlers) CreateBucketHandler(c echo.Context) error {
|
|
account := c.Get("account").(*db.Account)
|
|
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(400, map[string]string{"error": "Invalid request body"})
|
|
}
|
|
|
|
if req.Name == "" {
|
|
return c.JSON(400, map[string]string{"error": "Bucket name is required"})
|
|
}
|
|
|
|
bucket, err := h.bucketService.CreateBucket(context.Background(), req.Name, account.ID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "duplicate key") {
|
|
return c.JSON(409, map[string]string{"error": "Bucket name already exists"})
|
|
}
|
|
return c.JSON(500, map[string]string{"error": "Failed to create 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)
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": "Failed to list buckets"})
|
|
}
|
|
|
|
return c.JSON(200, map[string]any{
|
|
"buckets": buckets,
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) DeleteBucketHandler(c echo.Context) error {
|
|
user := c.Get("user").(*db.User)
|
|
bucketName := c.Param("bucket")
|
|
|
|
if err := h.bucketService.DeleteBucket(context.Background(), bucketName, user.ID); 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 delete bucket"})
|
|
}
|
|
|
|
return c.JSON(200, map[string]string{"message": "Bucket deleted successfully"})
|
|
}
|
|
|
|
func (h *Handlers) UploadObjectHandler(c echo.Context) error {
|
|
user := c.Get("user").(*db.User)
|
|
bucketName := c.Param("bucket")
|
|
objectKey := c.Param("*")
|
|
|
|
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"})
|
|
}
|
|
|
|
file, err := c.FormFile("file")
|
|
if err != nil {
|
|
return c.JSON(400, map[string]string{"error": "Failed to retrieve file"})
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": "Failed to open file"})
|
|
}
|
|
defer src.Close()
|
|
|
|
contentType := file.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
|
|
object, err := h.bucketService.UploadObject(context.Background(), bucket.ID, objectKey, file.Size, contentType, src)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "storage limit exceeded") {
|
|
return c.JSON(413, map[string]string{"error": "Storage limit exceeded"})
|
|
}
|
|
return c.JSON(500, map[string]string{"error": "Failed to upload 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 {
|
|
user := c.Get("user").(*db.User)
|
|
bucketName := c.Param("bucket")
|
|
objectKey := c.Param("*")
|
|
|
|
file, err := h.bucketService.GetObjectFile(context.Background(), bucketName, objectKey, user.ID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "no rows") {
|
|
return c.JSON(404, map[string]string{"error": "Object not found"})
|
|
}
|
|
return c.JSON(500, map[string]string{"error": "Failed to get object"})
|
|
}
|
|
defer file.Close()
|
|
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": "Failed to get file info"})
|
|
}
|
|
|
|
c.Response().Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
|
|
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", objectKey))
|
|
|
|
return c.Stream(http.StatusOK, "application/octet-stream", file)
|
|
}
|
|
func (h *Handlers) DeleteObjectHandler(c echo.Context) error {
|
|
user := c.Get("user").(*db.User)
|
|
bucketName := c.Param("bucket")
|
|
objectKey := c.Param("*")
|
|
|
|
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"})
|
|
}
|
|
|
|
if err := h.bucketService.DeleteObject(context.Background(), bucket.ID, objectKey); err != nil {
|
|
return c.JSON(500, map[string]string{"error": "Failed to delete object"})
|
|
}
|
|
|
|
return c.JSON(200, map[string]string{"message": "Object deleted successfully"})
|
|
}
|
|
|
|
func (h *Handlers) GetUserInfoHandler(c echo.Context) error {
|
|
user := c.Get("user").(*db.User)
|
|
|
|
buckets, err := h.bucketService.GetUserBuckets(context.Background(), user.ID)
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": "Failed to get user buckets"})
|
|
}
|
|
|
|
var totalUsage int64
|
|
for _, bucket := range buckets {
|
|
totalUsage += bucket.StorageUsedBytes
|
|
}
|
|
|
|
return c.JSON(200, map[string]any{
|
|
"user": user,
|
|
"totalUsage": totalUsage,
|
|
"bucketCount": len(buckets),
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) SetBucketPolicyHandler(c echo.Context) error {
|
|
user := c.Get("user").(*db.User)
|
|
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"})
|
|
}
|
|
|
|
var req struct {
|
|
Policy string `json:"policy"`
|
|
}
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(400, map[string]string{"error": "Invalid request body"})
|
|
}
|
|
|
|
if err := h.bucketService.SetBucketPolicy(context.Background(), bucket.ID, req.Policy); err != nil {
|
|
if strings.Contains(err.Error(), "invalid policy") {
|
|
return c.JSON(400, map[string]string{"error": err.Error()})
|
|
}
|
|
return c.JSON(500, map[string]string{"error": "Failed to set bucket policy"})
|
|
}
|
|
|
|
return c.JSON(200, map[string]string{"message": "Bucket policy set successfully"})
|
|
}
|
|
|
|
func (h *Handlers) GetBucketPolicyHandler(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"})
|
|
}
|
|
|
|
policyRecord, err := h.bucketService.GetBucketPolicy(context.Background(), bucket.ID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "no rows") {
|
|
return c.JSON(404, map[string]string{"error": "No policy found for bucket"})
|
|
}
|
|
return c.JSON(500, map[string]string{"error": "Failed to get bucket policy"})
|
|
}
|
|
|
|
return c.JSON(200, policyRecord)
|
|
}
|
|
|
|
func (h *Handlers) DeleteBucketPolicyHandler(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"})
|
|
}
|
|
|
|
if err := h.bucketService.DeleteBucketPolicy(context.Background(), bucket.ID); err != nil {
|
|
return c.JSON(500, map[string]string{"error": "Failed to delete bucket policy"})
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|