draft grpc

This commit is contained in:
2026-03-13 02:17:18 +00:00
parent ea2edbb9e0
commit 91e5e3542b
116 changed files with 44505 additions and 558 deletions

BIN
api Executable file

Binary file not shown.

BIN
bin/api Executable file

Binary file not shown.

19
buf.gen.yaml Normal file
View File

@@ -0,0 +1,19 @@
version: v2
plugins:
- remote: buf.build/protocolbuffers/go
out: internal/gen/proto
opt:
- paths=source_relative
- remote: buf.build/grpc/go
out: internal/gen/proto
opt:
- paths=source_relative
- remote: buf.build/community/stephenh-ts-proto
out: ../stream-ui/src/server/gen/proto
opt:
- env=node
- esModuleInterop=true
- outputServices=grpc-js
- useOptionals=all
- forceLong=number
- useDate=string

9
buf.yaml Normal file
View File

@@ -0,0 +1,9 @@
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
breaking:
use:
- FILE

View File

@@ -3,38 +3,20 @@ package main
import ( import (
"context" "context"
"log" "log"
"net/http" "net"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
"stream.api/internal/app"
"stream.api/internal/config" "stream.api/internal/config"
"stream.api/internal/database/query" "stream.api/internal/database/query"
videoruntime "stream.api/internal/video/runtime"
"stream.api/pkg/cache" "stream.api/pkg/cache"
"stream.api/pkg/database" "stream.api/pkg/database"
"stream.api/pkg/logger" "stream.api/pkg/logger"
"stream.api/pkg/token" "stream.api/pkg/token"
) )
// @title Stream API
// @version 1.0
// @description This is the API server for Stream application.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() { func main() {
// 1. Load Config // 1. Load Config
cfg, err := config.LoadConfig() cfg, err := config.LoadConfig()
@@ -52,6 +34,8 @@ func main() {
// Initialize generated query // Initialize generated query
query.SetDefault(db) query.SetDefault(db)
// TODO: Tách database migration ra luồng riêng nếu cần.
// 3. Connect Redis (Cache Interface) // 3. Connect Redis (Cache Interface)
rdb, err := cache.NewRedisCache(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB) rdb, err := cache.NewRedisCache(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB)
if err != nil { if err != nil {
@@ -63,39 +47,30 @@ func main() {
tokenProvider := token.NewJWTProvider(cfg.JWT.Secret) tokenProvider := token.NewJWTProvider(cfg.JWT.Secret)
appLogger := logger.NewLogger(cfg.Server.Mode) appLogger := logger.NewLogger(cfg.Server.Mode)
// 5. Setup Router module, err := videoruntime.NewModule(context.Background(), cfg, db, rdb, tokenProvider, appLogger)
r := app.SetupRouter(cfg, db, rdb, tokenProvider, appLogger) if err != nil {
log.Fatalf("Failed to setup gRPC runtime module: %v", err)
}
// 5. Run Server with Graceful Shutdown grpcListener, err := net.Listen("tcp", ":"+cfg.Server.GRPCPort)
srv := &http.Server{ if err != nil {
Addr: ":" + cfg.Server.Port, log.Fatalf("Failed to listen on gRPC port %s: %v", cfg.Server.GRPCPort, err)
Handler: r,
} }
go func() { go func() {
log.Printf("Starting server on port %s", cfg.Server.Port) log.Printf("Starting gRPC server on port %s", cfg.Server.GRPCPort)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := module.ServeGRPC(grpcListener); err != nil {
log.Fatalf("Failed to run server: %v", err) log.Fatalf("Failed to run gRPC server: %v", err)
} }
}() }()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be caught, so don't need to add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Println("Shutting down server...") log.Println("Shutting down gRPC server...")
// The context is used to inform the server it has 5 seconds to finish module.Shutdown()
// the request it is currently handling _ = grpcListener.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown: ", err)
}
log.Println("Server exiting") log.Println("Server exiting")
} }

89
cmd/gendb/main.go Normal file
View File

@@ -0,0 +1,89 @@
package main
import (
"log"
"os"
"gorm.io/driver/postgres"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/gorm"
)
func main() {
dsn := os.Getenv("APP_DATABASE_DSN")
if dsn == "" {
log.Fatal("APP_DATABASE_DSN is required")
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Không thể kết nối database:", err)
}
// 2. CẤU HÌNH GENERATOR
g := gen.NewGenerator(gen.Config{
OutPath: "internal/database/query",
ModelPkgPath: "internal/database/model",
Mode: gen.WithDefaultQuery | gen.WithQueryInterface, // Tạo cả query mặc định và interface để dễ mock test
// Tùy chọn sinh code cho Model
FieldNullable: true, // Sinh pointer (*) cho các field có thể NULL trong DB
FieldCoverable: true, // Sinh pointer cho tất cả field (hữu ích khi dùng hàm Update zero value)
FieldSignable: true, // Hỗ trợ unsigned integer
FieldWithIndexTag: true, // Sinh tag gorm:index
FieldWithTypeTag: true, // Sinh tag gorm:type
})
g.UseDB(db)
// 3. XỬ LÝ KIỂU DỮ LIỆU (Data Mapping)
// Ví dụ: Map decimal sang float64 thay vì string hoặc mảng byte
dataMap := map[string]func(gorm.ColumnType) (dataType string){
"decimal": func(columnType gorm.ColumnType) (dataType string) {
return "float64" // Hoặc "decimal.Decimal" nếu dùng shopspring/decimal
},
"numeric": func(columnType gorm.ColumnType) (dataType string) {
return "float64"
},
"text[]": func(columnType gorm.ColumnType) (dataType string) {
return "[]string"
},
"_text": func(columnType gorm.ColumnType) (dataType string) {
return "[]string"
},
}
g.WithDataTypeMap(dataMap)
g.WithImportPkgPath("github.com/lib/pq")
// 4. CÁC TÙY CHỌN (OPTIONS)
g.WithOpts(
gen.FieldType("id", "string"),
gen.FieldType("features", "pq.StringArray"),
gen.FieldGenType("features", "Field"),
gen.FieldGORMTag("version", func(tag field.GormTag) field.GormTag {
return tag.Set("version", "")
}),
gen.FieldJSONTag("password", "-"),
gen.FieldJSONTag("version", "-"),
)
// 5. CHỌN TABLE ĐỂ GENERATE
// GenerateAllTable() sẽ lấy tất cả, bao gồm cả bảng migration nếu có.
// Nếu muốn loại trừ bảng nào đó, hãy dùng logic filter hoặc liệt kê cụ thể.
// Cách 1: Lấy tất cả (như code cũ)
allTables := g.GenerateAllTable()
// Cách 2: (Khuyên dùng) Lọc bỏ các bảng rác hoặc hệ thống
// var tableModels []interface{}
// for _, tbl := range allTables {
// // Logic lọc bảng ở đây nếu cần
// tableModels = append(tableModels, tbl)
// }
g.ApplyBasic(allTables...)
g.Execute()
}

View File

@@ -1,5 +1,6 @@
server: server:
port: "8080" port: "8080"
grpc_port: "9000"
mode: "debug" # debug or release mode: "debug" # debug or release
database: database:
@@ -17,6 +18,20 @@ google:
client_id: "your_google_client_id" client_id: "your_google_client_id"
client_secret: "your_google_client_secret" client_secret: "your_google_client_secret"
redirect_url: "http://localhost:8080/auth/google/callback" redirect_url: "http://localhost:8080/auth/google/callback"
state_ttl_minutes: 10
frontend:
base_url: "http://localhost:5173"
google_auth_finalize_path: "/auth/google/finalize"
internal:
marker: "your_shared_internal_auth_marker"
cors:
allow_origins:
- "http://localhost:5173"
- "http://localhost:8080"
- "http://localhost:8081"
email: email:
from: "no-reply@picpic.com" from: "no-reply@picpic.com"
@@ -26,3 +41,10 @@ aws:
bucket: "your-bucket-name" bucket: "your-bucket-name"
access_key: "your_access_key" access_key: "your_access_key"
secret_key: "your_secret_key" secret_key: "your_secret_key"
render:
agent_secret: "your_render_agent_secret"
enable_metrics: true
enable_tracing: false
otlp_endpoint: ""
service_name: "stream-api-render"

View File

@@ -20,3 +20,6 @@ google:
email: email:
from: "no-reply@picpic.com" from: "no-reply@picpic.com"
internal:
marker: "your-secret-marker"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

46
go.mod
View File

@@ -7,18 +7,17 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/config v1.32.7
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1
github.com/gin-contrib/cors v1.7.6 github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lib/pq v1.11.2
github.com/redis/go-redis/v9 v9.17.2 github.com/redis/go-redis/v9 v9.17.2
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
golang.org/x/crypto v0.47.0 golang.org/x/crypto v0.47.0
golang.org/x/oauth2 v0.34.0 golang.org/x/oauth2 v0.34.0
google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.11
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gen v0.3.27 gorm.io/gen v0.3.27
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
@@ -28,7 +27,6 @@ require (
require ( require (
cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
@@ -44,47 +42,19 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/goccy/go-yaml v1.19.2 // 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/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
@@ -92,19 +62,15 @@ require (
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.32.0 // indirect golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gorm.io/datatypes v1.2.4 // indirect gorm.io/datatypes v1.2.4 // indirect
gorm.io/driver/mysql v1.5.7 // indirect gorm.io/driver/mysql v1.5.7 // indirect
gorm.io/hints v1.1.0 // indirect gorm.io/hints v1.1.0 // indirect

156
go.sum
View File

@@ -2,8 +2,6 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
@@ -46,90 +44,42 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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=
@@ -143,36 +93,21 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -190,85 +125,54 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -0,0 +1,390 @@
//go:build ignore
// +build ignore
package admin
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
// "stream.api/internal/database/model"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
type AdminAdTemplatePayload struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Description string `json:"description"`
VastTagURL string `json:"vast_tag_url"`
AdFormat string `json:"ad_format"`
Duration *int64 `json:"duration,omitempty"`
IsActive bool `json:"is_active"`
IsDefault bool `json:"is_default"`
OwnerEmail string `json:"owner_email,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type SaveAdminAdTemplateRequest struct {
UserID string `json:"user_id" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
VASTTagURL string `json:"vast_tag_url" binding:"required"`
AdFormat string `json:"ad_format"`
Duration *int64 `json:"duration"`
IsActive *bool `json:"is_active"`
IsDefault *bool `json:"is_default"`
}
func normalizeAdminAdFormat(value string) string {
switch strings.TrimSpace(strings.ToLower(value)) {
case "mid-roll", "post-roll":
return strings.TrimSpace(strings.ToLower(value))
default:
return "pre-roll"
}
}
func unsetAdminDefaultTemplates(tx *gorm.DB, userID, excludeID string) error {
query := tx.Model(&model.AdTemplate{}).Where("user_id = ?", userID)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
return query.Update("is_default", false).Error
}
func validateAdminAdTemplateRequest(req *SaveAdminAdTemplateRequest) string {
if strings.TrimSpace(req.UserID) == "" {
return "User ID is required"
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.VASTTagURL) == "" {
return "Name and VAST URL are required"
}
format := normalizeAdminAdFormat(req.AdFormat)
if format == "mid-roll" && (req.Duration == nil || *req.Duration <= 0) {
return "Duration is required for mid-roll templates"
}
return ""
}
func (h *Handler) buildAdminAdTemplatePayload(ctx *gin.Context, item model.AdTemplate, ownerEmail string) AdminAdTemplatePayload {
return AdminAdTemplatePayload{
ID: item.ID,
UserID: item.UserID,
Name: item.Name,
Description: adminStringValue(item.Description),
VastTagURL: item.VastTagURL,
AdFormat: adminStringValue(item.AdFormat),
Duration: item.Duration,
IsActive: adminBoolValue(item.IsActive, true),
IsDefault: item.IsDefault,
OwnerEmail: ownerEmail,
CreatedAt: adminFormatTime(item.CreatedAt),
UpdatedAt: adminFormatTime(item.UpdatedAt),
}
}
// @Summary List All Ad Templates
// @Description Get paginated list of all ad templates across users (admin only)
// @Tags admin
// @Produce json
// @Param page query int false "Page" default(1)
// @Param limit query int false "Limit" default(20)
// @Param user_id query string false "Filter by user ID"
// @Param search query string false "Search by name"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Router /admin/ad-templates [get]
// @Security BearerAuth
func (h *Handler) ListAdTemplates(c *gin.Context) {
ctx := c.Request.Context()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
offset := (page - 1) * limit
search := strings.TrimSpace(c.Query("search"))
userID := strings.TrimSpace(c.Query("user_id"))
db := h.db.WithContext(ctx).Model(&model.AdTemplate{})
if search != "" {
like := "%" + search + "%"
db = db.Where("name ILIKE ?", like)
}
if userID != "" {
db = db.Where("user_id = ?", userID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to list ad templates")
return
}
var templates []model.AdTemplate
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&templates).Error; err != nil {
h.logger.Error("Failed to list ad templates", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to list ad templates")
return
}
ownerIDs := map[string]bool{}
for _, t := range templates {
ownerIDs[t.UserID] = true
}
ownerEmails := map[string]string{}
if len(ownerIDs) > 0 {
ids := make([]string, 0, len(ownerIDs))
for id := range ownerIDs {
ids = append(ids, id)
}
var users []struct{ ID, Email string }
h.db.WithContext(ctx).Table("\"user\"").Select("id, email").Where("id IN ?", ids).Find(&users)
for _, u := range users {
ownerEmails[u.ID] = u.Email
}
}
result := make([]AdminAdTemplatePayload, 0, len(templates))
for _, t := range templates {
result = append(result, h.buildAdminAdTemplatePayload(c, t, ownerEmails[t.UserID]))
}
response.Success(c, gin.H{
"templates": result,
"total": total,
"page": page,
"limit": limit,
})
}
// @Summary Get Ad Template Detail
// @Description Get ad template detail (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "Ad Template ID"
// @Success 200 {object} response.Response
// @Router /admin/ad-templates/{id} [get]
// @Security BearerAuth
func (h *Handler) GetAdTemplate(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
var item model.AdTemplate
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to load ad template")
return
}
ownerEmail := ""
var user model.User
if err := h.db.WithContext(c.Request.Context()).Select("id, email").Where("id = ?", item.UserID).First(&user).Error; err == nil {
ownerEmail = user.Email
}
response.Success(c, gin.H{"template": h.buildAdminAdTemplatePayload(c, item, ownerEmail)})
}
// @Summary Create Ad Template
// @Description Create an ad template for any user (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param request body SaveAdminAdTemplateRequest true "Ad template payload"
// @Success 201 {object} response.Response
// @Router /admin/ad-templates [post]
// @Security BearerAuth
func (h *Handler) CreateAdTemplate(c *gin.Context) {
var req SaveAdminAdTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if msg := validateAdminAdTemplateRequest(&req); msg != "" {
response.Error(c, http.StatusBadRequest, msg)
return
}
ctx := c.Request.Context()
var user model.User
if err := h.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.UserID)).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusBadRequest, "User not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
item := &model.AdTemplate{
ID: uuid.New().String(),
UserID: user.ID,
Name: strings.TrimSpace(req.Name),
Description: adminStringPtr(req.Description),
VastTagURL: strings.TrimSpace(req.VASTTagURL),
AdFormat: model.StringPtr(normalizeAdminAdFormat(req.AdFormat)),
Duration: req.Duration,
IsActive: model.BoolPtr(req.IsActive == nil || *req.IsActive),
IsDefault: req.IsDefault != nil && *req.IsDefault,
}
if !adminBoolValue(item.IsActive, true) {
item.IsDefault = false
}
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetAdminDefaultTemplates(tx, item.UserID, ""); err != nil {
return err
}
}
return tx.Create(item).Error
}); err != nil {
h.logger.Error("Failed to create ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
response.Created(c, gin.H{"template": h.buildAdminAdTemplatePayload(c, *item, user.Email)})
}
// @Summary Update Ad Template
// @Description Update an ad template for any user (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "Ad Template ID"
// @Param request body SaveAdminAdTemplateRequest true "Ad template payload"
// @Success 200 {object} response.Response
// @Router /admin/ad-templates/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdateAdTemplate(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
var req SaveAdminAdTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if msg := validateAdminAdTemplateRequest(&req); msg != "" {
response.Error(c, http.StatusBadRequest, msg)
return
}
ctx := c.Request.Context()
var user model.User
if err := h.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.UserID)).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusBadRequest, "User not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
var item model.AdTemplate
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
item.UserID = user.ID
item.Name = strings.TrimSpace(req.Name)
item.Description = adminStringPtr(req.Description)
item.VastTagURL = strings.TrimSpace(req.VASTTagURL)
item.AdFormat = model.StringPtr(normalizeAdminAdFormat(req.AdFormat))
item.Duration = req.Duration
if req.IsActive != nil {
item.IsActive = model.BoolPtr(*req.IsActive)
}
if req.IsDefault != nil {
item.IsDefault = *req.IsDefault
}
if !adminBoolValue(item.IsActive, true) {
item.IsDefault = false
}
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetAdminDefaultTemplates(tx, item.UserID, item.ID); err != nil {
return err
}
}
return tx.Save(&item).Error
}); err != nil {
h.logger.Error("Failed to update ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
response.Success(c, gin.H{"template": h.buildAdminAdTemplatePayload(c, item, user.Email)})
}
// @Summary Delete Ad Template (Admin)
// @Description Delete any ad template by ID (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "Ad Template ID"
// @Success 200 {object} response.Response
// @Failure 404 {object} response.Response
// @Router /admin/ad-templates/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteAdTemplate(c *gin.Context) {
id := c.Param("id")
err := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("ad_template_id = ?", id).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
res := tx.Where("id = ?", id).Delete(&model.AdTemplate{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
if err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
h.logger.Error("Failed to delete ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to delete ad template")
return
}
response.Success(c, gin.H{"message": "Ad template deleted"})
}

View File

@@ -0,0 +1,94 @@
//go:build ignore
// +build ignore
package admin
import (
"fmt"
"strings"
"time"
)
func adminFormatTime(value *time.Time) string {
if value == nil {
return ""
}
return value.UTC().Format(time.RFC3339)
}
func adminFormatTimeValue(value time.Time) string {
if value.IsZero() {
return ""
}
return value.UTC().Format(time.RFC3339)
}
func adminStringPtr(value string) *string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
return &trimmed
}
func adminStringValue(value *string) string {
if value == nil {
return ""
}
return strings.TrimSpace(*value)
}
func adminInt64Ptr(value *int64) *int64 {
if value == nil {
return nil
}
return value
}
func adminStringSlice(values []string) []string {
if len(values) == 0 {
return nil
}
result := make([]string, 0, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
continue
}
result = append(result, trimmed)
}
if len(result) == 0 {
return nil
}
return result
}
func adminStringSliceValue(values []string) []string {
if len(values) == 0 {
return []string{}
}
return append([]string(nil), values...)
}
func adminBoolValue(value *bool, fallback bool) bool {
if value == nil {
return fallback
}
return *value
}
func adminInvoiceID(id string) string {
trimmed := strings.ReplaceAll(strings.TrimSpace(id), "-", "")
if len(trimmed) > 12 {
trimmed = trimmed[:12]
}
return "INV-" + strings.ToUpper(trimmed)
}
func adminTransactionID(prefix string) string {
return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano())
}

View File

@@ -0,0 +1,68 @@
//go:build ignore
// +build ignore
package admin
import (
"time"
"github.com/gin-gonic/gin"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
type DashboardPayload struct {
TotalUsers int64 `json:"total_users"`
TotalVideos int64 `json:"total_videos"`
TotalStorageUsed int64 `json:"total_storage_used"`
TotalPayments int64 `json:"total_payments"`
TotalRevenue float64 `json:"total_revenue"`
ActiveSubscriptions int64 `json:"active_subscriptions"`
TotalAdTemplates int64 `json:"total_ad_templates"`
NewUsersToday int64 `json:"new_users_today"`
NewVideosToday int64 `json:"new_videos_today"`
}
// @Summary Admin Dashboard
// @Description Get system-wide statistics for the admin dashboard
// @Tags admin
// @Produce json
// @Success 200 {object} response.Response{data=DashboardPayload}
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Router /admin/dashboard [get]
// @Security BearerAuth
func (h *Handler) Dashboard(c *gin.Context) {
ctx := c.Request.Context()
var payload DashboardPayload
h.db.WithContext(ctx).Model(&model.User{}).Count(&payload.TotalUsers)
h.db.WithContext(ctx).Model(&model.Video{}).Count(&payload.TotalVideos)
h.db.WithContext(ctx).Model(&model.User{}).
Select("COALESCE(SUM(storage_used), 0)").
Row().Scan(&payload.TotalStorageUsed)
h.db.WithContext(ctx).Model(&model.Payment{}).Count(&payload.TotalPayments)
h.db.WithContext(ctx).Model(&model.Payment{}).
Where("status = ?", "SUCCESS").
Select("COALESCE(SUM(amount), 0)").
Row().Scan(&payload.TotalRevenue)
h.db.WithContext(ctx).Model(&model.PlanSubscription{}).
Where("expires_at > ?", time.Now()).
Count(&payload.ActiveSubscriptions)
h.db.WithContext(ctx).Model(&model.AdTemplate{}).Count(&payload.TotalAdTemplates)
today := time.Now().Truncate(24 * time.Hour)
h.db.WithContext(ctx).Model(&model.User{}).
Where("created_at >= ?", today).
Count(&payload.NewUsersToday)
h.db.WithContext(ctx).Model(&model.Video{}).
Where("created_at >= ?", today).
Count(&payload.NewVideosToday)
response.Success(c, payload)
}

View File

@@ -0,0 +1,26 @@
//go:build ignore
// +build ignore
package admin
import (
"gorm.io/gorm"
runtimegrpc "stream.api/internal/video/runtime/grpc"
"stream.api/internal/video/runtime/services"
"stream.api/pkg/logger"
)
type RenderRuntime interface {
JobService() *services.JobService
AgentRuntime() *runtimegrpc.Server
}
type Handler struct {
logger logger.Logger
db *gorm.DB
runtime RenderRuntime
}
func NewHandler(l logger.Logger, db *gorm.DB, renderRuntime RenderRuntime) *Handler {
return &Handler{logger: l, db: db, runtime: renderRuntime}
}

View File

@@ -0,0 +1,521 @@
//go:build ignore
// +build ignore
package admin
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
const (
adminWalletTransactionTypeTopup = "topup"
adminWalletTransactionTypeSubscriptionDebit = "subscription_debit"
adminPaymentMethodWallet = "wallet"
adminPaymentMethodTopup = "topup"
)
var adminAllowedTermMonths = map[int32]struct{}{
1: {}, 3: {}, 6: {}, 12: {},
}
type AdminPaymentPayload struct {
ID string `json:"id"`
UserID string `json:"user_id"`
PlanID *string `json:"plan_id,omitempty"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"`
Provider string `json:"provider"`
TransactionID string `json:"transaction_id,omitempty"`
UserEmail string `json:"user_email,omitempty"`
PlanName string `json:"plan_name,omitempty"`
InvoiceID string `json:"invoice_id"`
TermMonths *int32 `json:"term_months,omitempty"`
PaymentMethod *string `json:"payment_method,omitempty"`
ExpiresAt *string `json:"expires_at,omitempty"`
WalletAmount *float64 `json:"wallet_amount,omitempty"`
TopupAmount *float64 `json:"topup_amount,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type CreateAdminPaymentRequest struct {
UserID string `json:"user_id" binding:"required"`
PlanID string `json:"plan_id" binding:"required"`
TermMonths int32 `json:"term_months" binding:"required"`
PaymentMethod string `json:"payment_method" binding:"required"`
TopupAmount *float64 `json:"topup_amount,omitempty"`
}
type UpdateAdminPaymentRequest struct {
Status string `json:"status" binding:"required"`
}
func normalizeAdminPaymentMethod(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case adminPaymentMethodWallet:
return adminPaymentMethodWallet
case adminPaymentMethodTopup:
return adminPaymentMethodTopup
default:
return ""
}
}
func normalizeAdminPaymentStatus(value string) string {
switch strings.ToUpper(strings.TrimSpace(value)) {
case "PENDING":
return "PENDING"
case "FAILED":
return "FAILED"
default:
return "SUCCESS"
}
}
func adminIsAllowedTermMonths(value int32) bool {
_, ok := adminAllowedTermMonths[value]
return ok
}
func (h *Handler) loadAdminPaymentPayload(ctx *gin.Context, payment model.Payment) (AdminPaymentPayload, error) {
payload := AdminPaymentPayload{
ID: payment.ID,
UserID: payment.UserID,
PlanID: payment.PlanID,
Amount: payment.Amount,
Currency: strings.ToUpper(adminStringValue(payment.Currency)),
Status: normalizeAdminPaymentStatus(adminStringValue(payment.Status)),
Provider: strings.ToUpper(adminStringValue(payment.Provider)),
TransactionID: adminStringValue(payment.TransactionID),
InvoiceID: adminInvoiceID(payment.ID),
CreatedAt: adminFormatTime(payment.CreatedAt),
UpdatedAt: adminFormatTimeValue(payment.UpdatedAt),
}
if payload.Currency == "" {
payload.Currency = "USD"
}
var user model.User
if err := h.db.WithContext(ctx.Request.Context()).Select("id, email").Where("id = ?", payment.UserID).First(&user).Error; err == nil {
payload.UserEmail = user.Email
}
if payment.PlanID != nil && strings.TrimSpace(*payment.PlanID) != "" {
var plan model.Plan
if err := h.db.WithContext(ctx.Request.Context()).Where("id = ?", *payment.PlanID).First(&plan).Error; err == nil {
payload.PlanName = plan.Name
}
}
var subscription model.PlanSubscription
if err := h.db.WithContext(ctx.Request.Context()).Where("payment_id = ?", payment.ID).Order("created_at DESC").First(&subscription).Error; err == nil {
payload.TermMonths = &subscription.TermMonths
method := subscription.PaymentMethod
payload.PaymentMethod = &method
expiresAt := subscription.ExpiresAt.UTC().Format(time.RFC3339)
payload.ExpiresAt = &expiresAt
payload.WalletAmount = &subscription.WalletAmount
payload.TopupAmount = &subscription.TopupAmount
}
return payload, nil
}
func (h *Handler) adminLockUserForUpdate(ctx *gin.Context, tx *gorm.DB, userID string) (*model.User, error) {
var user model.User
if err := tx.WithContext(ctx.Request.Context()).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", userID).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (h *Handler) adminCreateSubscriptionPayment(ctx *gin.Context, req CreateAdminPaymentRequest) (*model.Payment, *model.PlanSubscription, float64, error) {
planID := strings.TrimSpace(req.PlanID)
userID := strings.TrimSpace(req.UserID)
paymentMethod := normalizeAdminPaymentMethod(req.PaymentMethod)
if paymentMethod == "" {
return nil, nil, 0, errors.New("Payment method must be wallet or topup")
}
if !adminIsAllowedTermMonths(req.TermMonths) {
return nil, nil, 0, errors.New("Term months must be one of 1, 3, 6, or 12")
}
var planRecord model.Plan
if err := h.db.WithContext(ctx.Request.Context()).Where("id = ?", planID).First(&planRecord).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, 0, errors.New("Plan not found")
}
return nil, nil, 0, err
}
if !adminBoolValue(planRecord.IsActive, true) {
return nil, nil, 0, errors.New("Plan is not active")
}
var user model.User
if err := h.db.WithContext(ctx.Request.Context()).Where("id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, 0, errors.New("User not found")
}
return nil, nil, 0, err
}
totalAmount := planRecord.Price * float64(req.TermMonths)
status := "SUCCESS"
provider := "INTERNAL"
currency := "USD"
transactionID := adminTransactionID("sub")
now := time.Now().UTC()
payment := &model.Payment{
ID: uuid.New().String(),
UserID: user.ID,
PlanID: &planRecord.ID,
Amount: totalAmount,
Currency: &currency,
Status: &status,
Provider: &provider,
TransactionID: &transactionID,
}
var subscription *model.PlanSubscription
var walletBalance float64
err := h.db.WithContext(ctx.Request.Context()).Transaction(func(tx *gorm.DB) error {
if _, err := h.adminLockUserForUpdate(ctx, tx, user.ID); err != nil {
return err
}
var currentSubscription model.PlanSubscription
hasCurrentSubscription := false
if err := tx.Where("user_id = ?", user.ID).Order("created_at DESC").First(&currentSubscription).Error; err == nil {
hasCurrentSubscription = true
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
baseExpiry := now
if hasCurrentSubscription && currentSubscription.ExpiresAt.After(baseExpiry) {
baseExpiry = currentSubscription.ExpiresAt.UTC()
}
newExpiry := baseExpiry.AddDate(0, int(req.TermMonths), 0)
currentWalletBalance, err := model.GetWalletBalance(ctx.Request.Context(), tx, user.ID)
if err != nil {
return err
}
shortfall := totalAmount - currentWalletBalance
if shortfall < 0 {
shortfall = 0
}
if paymentMethod == adminPaymentMethodWallet && shortfall > 0 {
return fmt.Errorf("Insufficient wallet balance")
}
topupAmount := 0.0
if paymentMethod == adminPaymentMethodTopup {
if req.TopupAmount == nil {
return fmt.Errorf("Top-up amount is required when payment method is topup")
}
topupAmount = *req.TopupAmount
if topupAmount <= 0 {
return fmt.Errorf("Top-up amount must be greater than 0")
}
if topupAmount < shortfall {
return fmt.Errorf("Top-up amount must be greater than or equal to the required shortfall")
}
}
if err := tx.Create(payment).Error; err != nil {
return err
}
if paymentMethod == adminPaymentMethodTopup {
topupTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: user.ID,
Type: adminWalletTransactionTypeTopup,
Amount: topupAmount,
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Wallet top-up for %s (%d months)", planRecord.Name, req.TermMonths)),
PaymentID: &payment.ID,
PlanID: &planRecord.ID,
TermMonths: &req.TermMonths,
}
if err := tx.Create(topupTransaction).Error; err != nil {
return err
}
}
debitTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: user.ID,
Type: adminWalletTransactionTypeSubscriptionDebit,
Amount: -totalAmount,
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Subscription payment for %s (%d months)", planRecord.Name, req.TermMonths)),
PaymentID: &payment.ID,
PlanID: &planRecord.ID,
TermMonths: &req.TermMonths,
}
if err := tx.Create(debitTransaction).Error; err != nil {
return err
}
subscription = &model.PlanSubscription{
ID: uuid.New().String(),
UserID: user.ID,
PaymentID: payment.ID,
PlanID: planRecord.ID,
TermMonths: req.TermMonths,
PaymentMethod: paymentMethod,
WalletAmount: totalAmount,
TopupAmount: topupAmount,
StartedAt: now,
ExpiresAt: newExpiry,
}
if err := tx.Create(subscription).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Update("plan_id", planRecord.ID).Error; err != nil {
return err
}
notification := &model.Notification{
ID: uuid.New().String(),
UserID: user.ID,
Type: "billing.subscription",
Title: "Subscription activated",
Message: fmt.Sprintf("Your subscription to %s is active until %s.", planRecord.Name, subscription.ExpiresAt.UTC().Format("2006-01-02")),
Metadata: model.StringPtr("{}"),
}
if err := tx.Create(notification).Error; err != nil {
return err
}
walletBalance, err = model.GetWalletBalance(ctx.Request.Context(), tx, user.ID)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, nil, 0, err
}
return payment, subscription, walletBalance, nil
}
// @Summary List All Payments
// @Description Get paginated list of all payments across users (admin only)
// @Tags admin
// @Produce json
// @Param page query int false "Page" default(1)
// @Param limit query int false "Limit" default(20)
// @Param user_id query string false "Filter by user ID"
// @Param status query string false "Filter by status"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Router /admin/payments [get]
// @Security BearerAuth
func (h *Handler) ListPayments(c *gin.Context) {
ctx := c.Request.Context()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
offset := (page - 1) * limit
userID := strings.TrimSpace(c.Query("user_id"))
status := strings.TrimSpace(c.Query("status"))
db := h.db.WithContext(ctx).Model(&model.Payment{})
if userID != "" {
db = db.Where("user_id = ?", userID)
}
if status != "" {
db = db.Where("UPPER(status) = ?", strings.ToUpper(status))
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to list payments")
return
}
var payments []model.Payment
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&payments).Error; err != nil {
h.logger.Error("Failed to list payments", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to list payments")
return
}
result := make([]AdminPaymentPayload, 0, len(payments))
for _, p := range payments {
payload, err := h.loadAdminPaymentPayload(c, p)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to list payments")
return
}
result = append(result, payload)
}
response.Success(c, gin.H{
"payments": result,
"total": total,
"page": page,
"limit": limit,
})
}
// @Summary Get Payment Detail
// @Description Get payment detail (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "Payment ID"
// @Success 200 {object} response.Response
// @Router /admin/payments/{id} [get]
// @Security BearerAuth
func (h *Handler) GetPayment(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Payment not found")
return
}
var payment model.Payment
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&payment).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Payment not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to get payment")
return
}
payload, err := h.loadAdminPaymentPayload(c, payment)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to get payment")
return
}
response.Success(c, gin.H{"payment": payload})
}
// @Summary Create Payment
// @Description Create a model subscription charge for a user (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param request body CreateAdminPaymentRequest true "Payment payload"
// @Success 201 {object} response.Response
// @Router /admin/payments [post]
// @Security BearerAuth
func (h *Handler) CreatePayment(c *gin.Context) {
var req CreateAdminPaymentRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
payment, subscription, walletBalance, err := h.adminCreateSubscriptionPayment(c, req)
if err != nil {
switch err.Error() {
case "User not found", "Plan not found", "Plan is not active", "Payment method must be wallet or topup", "Term months must be one of 1, 3, 6, or 12", "Insufficient wallet balance", "Top-up amount is required when payment method is topup", "Top-up amount must be greater than 0", "Top-up amount must be greater than or equal to the required shortfall":
response.Error(c, http.StatusBadRequest, err.Error())
return
default:
h.logger.Error("Failed to create admin payment", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create payment")
return
}
}
payload, err := h.loadAdminPaymentPayload(c, *payment)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to create payment")
return
}
response.Created(c, gin.H{
"payment": payload,
"subscription": subscription,
"wallet_balance": walletBalance,
"invoice_id": adminInvoiceID(payment.ID),
})
}
// @Summary Update Payment
// @Description Update payment status safely without hard delete (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "Payment ID"
// @Param request body UpdateAdminPaymentRequest true "Payment update payload"
// @Success 200 {object} response.Response
// @Router /admin/payments/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdatePayment(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Payment not found")
return
}
var req UpdateAdminPaymentRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
newStatus := normalizeAdminPaymentStatus(req.Status)
var payment model.Payment
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&payment).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Payment not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to update payment")
return
}
currentStatus := normalizeAdminPaymentStatus(adminStringValue(payment.Status))
if currentStatus != newStatus {
if (currentStatus == "FAILED" || currentStatus == "PENDING") && newStatus == "SUCCESS" {
response.Error(c, http.StatusBadRequest, "Cannot transition payment to SUCCESS from admin update; recreate through the payment flow instead")
return
}
payment.Status = &newStatus
if err := h.db.WithContext(c.Request.Context()).Save(&payment).Error; err != nil {
h.logger.Error("Failed to update payment", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update payment")
return
}
}
payload, err := h.loadAdminPaymentPayload(c, payment)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to update payment")
return
}
response.Success(c, gin.H{"payment": payload})
}

302
internal/api/admin/plans.go Normal file
View File

@@ -0,0 +1,302 @@
//go:build ignore
// +build ignore
package admin
import (
"errors"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
type AdminPlanPayload struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Features []string `json:"features,omitempty"`
Price float64 `json:"price"`
Cycle string `json:"cycle"`
StorageLimit int64 `json:"storage_limit"`
UploadLimit int32 `json:"upload_limit"`
DurationLimit int32 `json:"duration_limit"`
QualityLimit string `json:"quality_limit"`
IsActive bool `json:"is_active"`
UserCount int64 `json:"user_count"`
PaymentCount int64 `json:"payment_count"`
SubscriptionCount int64 `json:"subscription_count"`
}
type SavePlanRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Features []string `json:"features"`
Price float64 `json:"price" binding:"required"`
Cycle string `json:"cycle" binding:"required"`
StorageLimit int64 `json:"storage_limit" binding:"required"`
UploadLimit int32 `json:"upload_limit" binding:"required"`
IsActive *bool `json:"is_active"`
}
func buildAdminPlanPayload(plan model.Plan, userCount, paymentCount, subscriptionCount int64) AdminPlanPayload {
return AdminPlanPayload{
ID: plan.ID,
Name: plan.Name,
Description: adminStringValue(plan.Description),
Features: adminStringSliceValue(plan.Features),
Price: plan.Price,
Cycle: plan.Cycle,
StorageLimit: plan.StorageLimit,
UploadLimit: plan.UploadLimit,
DurationLimit: plan.DurationLimit,
QualityLimit: plan.QualityLimit,
IsActive: adminBoolValue(plan.IsActive, true),
UserCount: userCount,
PaymentCount: paymentCount,
SubscriptionCount: subscriptionCount,
}
}
func (h *Handler) loadPlanUsageCounts(ctx *gin.Context, planID string) (int64, int64, int64, error) {
var userCount int64
if err := h.db.WithContext(ctx.Request.Context()).Model(&model.User{}).Where("plan_id = ?", planID).Count(&userCount).Error; err != nil {
return 0, 0, 0, err
}
var paymentCount int64
if err := h.db.WithContext(ctx.Request.Context()).Model(&model.Payment{}).Where("plan_id = ?", planID).Count(&paymentCount).Error; err != nil {
return 0, 0, 0, err
}
var subscriptionCount int64
if err := h.db.WithContext(ctx.Request.Context()).Model(&model.PlanSubscription{}).Where("plan_id = ?", planID).Count(&subscriptionCount).Error; err != nil {
return 0, 0, 0, err
}
return userCount, paymentCount, subscriptionCount, nil
}
func validatePlanRequest(req *SavePlanRequest) string {
if strings.TrimSpace(req.Name) == "" {
return "Name is required"
}
if strings.TrimSpace(req.Cycle) == "" {
return "Cycle is required"
}
if req.Price < 0 {
return "Price must be greater than or equal to 0"
}
if req.StorageLimit <= 0 {
return "Storage limit must be greater than 0"
}
if req.UploadLimit <= 0 {
return "Upload limit must be greater than 0"
}
return ""
}
// @Summary List Plans
// @Description Get all plans with usage counts (admin only)
// @Tags admin
// @Produce json
// @Success 200 {object} response.Response
// @Router /admin/plans [get]
// @Security BearerAuth
func (h *Handler) ListPlans(c *gin.Context) {
ctx := c.Request.Context()
var plans []model.Plan
if err := h.db.WithContext(ctx).Order("price ASC").Find(&plans).Error; err != nil {
h.logger.Error("Failed to list plans", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to list plans")
return
}
result := make([]AdminPlanPayload, 0, len(plans))
for _, plan := range plans {
userCount, paymentCount, subscriptionCount, err := h.loadPlanUsageCounts(c, plan.ID)
if err != nil {
h.logger.Error("Failed to load plan usage", "error", err, "plan_id", plan.ID)
response.Error(c, http.StatusInternalServerError, "Failed to list plans")
return
}
result = append(result, buildAdminPlanPayload(plan, userCount, paymentCount, subscriptionCount))
}
response.Success(c, gin.H{"plans": result})
}
// @Summary Create Plan
// @Description Create a plan (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param request body SavePlanRequest true "Plan payload"
// @Success 201 {object} response.Response
// @Router /admin/plans [post]
// @Security BearerAuth
func (h *Handler) CreatePlan(c *gin.Context) {
var req SavePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if msg := validatePlanRequest(&req); msg != "" {
response.Error(c, http.StatusBadRequest, msg)
return
}
plan := &model.Plan{
ID: uuid.New().String(),
Name: strings.TrimSpace(req.Name),
Description: adminStringPtr(req.Description),
Features: adminStringSlice(req.Features),
Price: req.Price,
Cycle: strings.TrimSpace(req.Cycle),
StorageLimit: req.StorageLimit,
UploadLimit: req.UploadLimit,
DurationLimit: 0,
QualityLimit: "",
IsActive: func() *bool {
value := true
if req.IsActive != nil {
value = *req.IsActive
}
return &value
}(),
}
if err := h.db.WithContext(c.Request.Context()).Create(plan).Error; err != nil {
h.logger.Error("Failed to create plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create plan")
return
}
response.Created(c, gin.H{"plan": buildAdminPlanPayload(*plan, 0, 0, 0)})
}
// @Summary Update Plan
// @Description Update a plan (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "Plan ID"
// @Param request body SavePlanRequest true "Plan payload"
// @Success 200 {object} response.Response
// @Router /admin/plans/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdatePlan(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
var req SavePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if msg := validatePlanRequest(&req); msg != "" {
response.Error(c, http.StatusBadRequest, msg)
return
}
ctx := c.Request.Context()
var plan model.Plan
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to update plan")
return
}
plan.Name = strings.TrimSpace(req.Name)
plan.Description = adminStringPtr(req.Description)
plan.Features = adminStringSlice(req.Features)
plan.Price = req.Price
plan.Cycle = strings.TrimSpace(req.Cycle)
plan.StorageLimit = req.StorageLimit
plan.UploadLimit = req.UploadLimit
if req.IsActive != nil {
plan.IsActive = req.IsActive
}
if err := h.db.WithContext(ctx).Save(&plan).Error; err != nil {
h.logger.Error("Failed to update plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update plan")
return
}
userCount, paymentCount, subscriptionCount, err := h.loadPlanUsageCounts(c, plan.ID)
if err != nil {
h.logger.Error("Failed to load plan usage", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update plan")
return
}
response.Success(c, gin.H{"plan": buildAdminPlanPayload(plan, userCount, paymentCount, subscriptionCount)})
}
// @Summary Delete Plan
// @Description Delete a plan, or deactivate it if already used (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "Plan ID"
// @Success 200 {object} response.Response
// @Router /admin/plans/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeletePlan(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
ctx := c.Request.Context()
var plan model.Plan
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
var paymentCount int64
if err := h.db.WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", id).Count(&paymentCount).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
var subscriptionCount int64
if err := h.db.WithContext(ctx).Model(&model.PlanSubscription{}).Where("plan_id = ?", id).Count(&subscriptionCount).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
if paymentCount > 0 || subscriptionCount > 0 {
inactive := false
if err := h.db.WithContext(ctx).Model(&model.Plan{}).Where("id = ?", id).Update("is_active", inactive).Error; err != nil {
h.logger.Error("Failed to deactivate plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to deactivate plan")
return
}
response.Success(c, gin.H{"message": "Plan deactivated", "mode": "deactivated"})
return
}
if err := h.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Plan{}).Error; err != nil {
h.logger.Error("Failed to delete plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to delete plan")
return
}
response.Success(c, gin.H{"message": "Plan deleted", "mode": "deleted"})
}

View File

@@ -0,0 +1,218 @@
//go:build ignore
// +build ignore
package admin
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"stream.api/pkg/response"
)
type createJobRequest struct {
Command string `json:"command"`
Image string `json:"image"`
Env map[string]string `json:"env"`
Priority int `json:"priority"`
UserID string `json:"user_id"`
Name string `json:"name"`
TimeLimit int64 `json:"time_limit"`
}
// @Summary List render jobs
// @Description Returns paginated render jobs for admin management
// @Tags admin-render
// @Security BearerAuth
// @Produce json
// @Param offset query int false "Offset"
// @Param limit query int false "Limit"
// @Param agent_id query string false "Agent ID"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/jobs [get]
func (h *Handler) ListJobs(c *gin.Context) {
offset := parseInt(c.Query("offset"), 0)
limit := parseInt(c.Query("limit"), 20)
if agentID := c.Query("agent_id"); agentID != "" {
items, err := h.runtime.JobService().ListJobsByAgent(c.Request.Context(), agentID, offset, limit)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to list jobs")
return
}
response.Success(c, items)
return
}
items, err := h.runtime.JobService().ListJobs(c.Request.Context(), offset, limit)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to list jobs")
return
}
response.Success(c, items)
}
// @Summary Get render job detail
// @Description Returns a render job by ID
// @Tags admin-render
// @Security BearerAuth
// @Produce json
// @Param id path string true "Job ID"
// @Success 200 {object} response.Response
// @Failure 404 {object} response.Response
// @Router /admin/jobs/{id} [get]
func (h *Handler) GetJob(c *gin.Context) {
job, err := h.runtime.JobService().GetJob(c.Request.Context(), c.Param("id"))
if err != nil {
response.Error(c, http.StatusNotFound, "Job not found")
return
}
response.Success(c, gin.H{"job": job})
}
// @Summary Get render job logs
// @Description Returns plain text logs for a render job
// @Tags admin-render
// @Security BearerAuth
// @Produce plain
// @Param id path string true "Job ID"
// @Success 200 {string} string
// @Failure 404 {object} response.Response
// @Router /admin/jobs/{id}/logs [get]
func (h *Handler) GetJobLogs(c *gin.Context) {
job, err := h.runtime.JobService().GetJob(c.Request.Context(), c.Param("id"))
if err != nil {
response.Error(c, http.StatusNotFound, "Job not found")
return
}
c.String(http.StatusOK, job.Logs)
}
// @Summary Create render job
// @Description Queues a new render job for agents
// @Tags admin-render
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param payload body createJobRequest true "Job payload"
// @Success 201 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/jobs [post]
func (h *Handler) CreateJob(c *gin.Context) {
var req createJobRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if req.Command == "" {
response.Error(c, http.StatusBadRequest, "Command is required")
return
}
if req.Image == "" {
req.Image = "alpine"
}
if req.Name == "" {
req.Name = req.Command
}
payload, _ := json.Marshal(map[string]interface{}{"image": req.Image, "commands": []string{req.Command}, "environment": req.Env})
job, err := h.runtime.JobService().CreateJob(c.Request.Context(), req.UserID, req.Name, payload, req.Priority, req.TimeLimit)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to create job")
return
}
response.Created(c, gin.H{"job": job})
}
// @Summary Cancel render job
// @Description Cancels a pending or running render job
// @Tags admin-render
// @Security BearerAuth
// @Produce json
// @Param id path string true "Job ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/jobs/{id}/cancel [post]
func (h *Handler) CancelJob(c *gin.Context) {
if err := h.runtime.JobService().CancelJob(c.Request.Context(), c.Param("id")); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, gin.H{"status": "cancelled", "job_id": c.Param("id")})
}
// @Summary Retry render job
// @Description Retries a failed or cancelled render job
// @Tags admin-render
// @Security BearerAuth
// @Produce json
// @Param id path string true "Job ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /admin/jobs/{id}/retry [post]
func (h *Handler) RetryJob(c *gin.Context) {
job, err := h.runtime.JobService().RetryJob(c.Request.Context(), c.Param("id"))
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, gin.H{"job": job})
}
// @Summary List connected render agents
// @Description Returns currently connected render agents and current runtime stats
// @Tags admin-render
// @Security BearerAuth
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/agents [get]
func (h *Handler) ListAgents(c *gin.Context) {
response.Success(c, gin.H{"agents": h.runtime.AgentRuntime().ListAgentsWithStats()})
}
// @Summary Restart connected render agent
// @Description Sends a restart command to a currently connected render agent
// @Tags admin-render
// @Security BearerAuth
// @Produce json
// @Param id path string true "Agent ID"
// @Success 200 {object} response.Response
// @Failure 503 {object} response.Response
// @Router /admin/agents/{id}/restart [post]
func (h *Handler) RestartAgent(c *gin.Context) {
if ok := h.runtime.AgentRuntime().SendCommand(c.Param("id"), "restart"); !ok {
response.Error(c, http.StatusServiceUnavailable, "Agent not active or command channel full")
return
}
response.Success(c, gin.H{"status": "restart command sent"})
}
// @Summary Update connected render agent
// @Description Sends an update command to a currently connected render agent
// @Tags admin-render
// @Security BearerAuth
// @Produce json
// @Param id path string true "Agent ID"
// @Success 200 {object} response.Response
// @Failure 503 {object} response.Response
// @Router /admin/agents/{id}/update [post]
func (h *Handler) UpdateAgent(c *gin.Context) {
if ok := h.runtime.AgentRuntime().SendCommand(c.Param("id"), "update"); !ok {
response.Error(c, http.StatusServiceUnavailable, "Agent not active or command channel full")
return
}
response.Success(c, gin.H{"status": "update command sent"})
}
func parseInt(value string, fallback int) int {
if value == "" {
return fallback
}
var result int
if _, err := fmt.Sscanf(value, "%d", &result); err != nil {
return fallback
}
return result
}

522
internal/api/admin/users.go Normal file
View File

@@ -0,0 +1,522 @@
//go:build ignore
// +build ignore
package admin
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
type AdminUserPayload struct {
ID string `json:"id"`
Email string `json:"email"`
Username *string `json:"username"`
Avatar *string `json:"avatar"`
Role *string `json:"role"`
PlanID *string `json:"plan_id"`
PlanName string `json:"plan_name,omitempty"`
StorageUsed int64 `json:"storage_used"`
VideoCount int64 `json:"video_count"`
WalletBalance float64 `json:"wallet_balance"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type CreateAdminUserRequest struct {
Email string `json:"email" binding:"required,email"`
Username string `json:"username"`
Password string `json:"password" binding:"required,min=6"`
Role string `json:"role"`
PlanID *string `json:"plan_id"`
}
type UpdateAdminUserRequest struct {
Email *string `json:"email"`
Username *string `json:"username"`
Password *string `json:"password"`
Role *string `json:"role"`
PlanID *string `json:"plan_id"`
}
type UpdateUserRoleRequest struct {
Role string `json:"role" binding:"required"`
}
func normalizeAdminRole(value string) string {
role := strings.ToUpper(strings.TrimSpace(value))
if role == "" {
return "USER"
}
return role
}
func isValidAdminRole(role string) bool {
switch normalizeAdminRole(role) {
case "USER", "ADMIN", "BLOCK":
return true
default:
return false
}
}
func (h *Handler) ensurePlanExists(ctx *gin.Context, planID *string) error {
if planID == nil {
return nil
}
trimmed := strings.TrimSpace(*planID)
if trimmed == "" {
return nil
}
var count int64
if err := h.db.WithContext(ctx.Request.Context()).Model(&model.Plan{}).Where("id = ?", trimmed).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// @Summary List Users
// @Description Get paginated list of all users (admin only)
// @Tags admin
// @Produce json
// @Param page query int false "Page" default(1)
// @Param limit query int false "Limit" default(20)
// @Param search query string false "Search by email or username"
// @Param role query string false "Filter by role"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Router /admin/users [get]
// @Security BearerAuth
func (h *Handler) ListUsers(c *gin.Context) {
ctx := c.Request.Context()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
offset := (page - 1) * limit
search := strings.TrimSpace(c.Query("search"))
role := strings.TrimSpace(c.Query("role"))
db := h.db.WithContext(ctx).Model(&model.User{})
if search != "" {
like := "%" + search + "%"
db = db.Where("email ILIKE ? OR username ILIKE ?", like, like)
}
if role != "" {
db = db.Where("UPPER(role) = ?", strings.ToUpper(role))
}
var total int64
if err := db.Count(&total).Error; err != nil {
h.logger.Error("Failed to count users", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to list users")
return
}
var users []model.User
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&users).Error; err != nil {
h.logger.Error("Failed to list users", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to list users")
return
}
planIDs := map[string]bool{}
for _, u := range users {
if u.PlanID != nil && strings.TrimSpace(*u.PlanID) != "" {
planIDs[*u.PlanID] = true
}
}
planNames := map[string]string{}
if len(planIDs) > 0 {
ids := make([]string, 0, len(planIDs))
for id := range planIDs {
ids = append(ids, id)
}
var plans []model.Plan
h.db.WithContext(ctx).Where("id IN ?", ids).Find(&plans)
for _, p := range plans {
planNames[p.ID] = p.Name
}
}
result := make([]AdminUserPayload, 0, len(users))
for _, u := range users {
payload := AdminUserPayload{
ID: u.ID,
Email: u.Email,
Username: u.Username,
Avatar: u.Avatar,
Role: u.Role,
PlanID: u.PlanID,
StorageUsed: u.StorageUsed,
CreatedAt: adminFormatTime(u.CreatedAt),
UpdatedAt: adminFormatTimeValue(u.UpdatedAt),
}
if u.PlanID != nil {
payload.PlanName = planNames[*u.PlanID]
}
h.db.WithContext(ctx).Model(&model.Video{}).Where("user_id = ?", u.ID).Count(&payload.VideoCount)
payload.WalletBalance, _ = model.GetWalletBalance(ctx, h.db, u.ID)
result = append(result, payload)
}
response.Success(c, gin.H{
"users": result,
"total": total,
"page": page,
"limit": limit,
})
}
// @Summary Create User
// @Description Create a user from admin panel (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param request body CreateAdminUserRequest true "User payload"
// @Success 201 {object} response.Response
// @Router /admin/users [post]
// @Security BearerAuth
func (h *Handler) CreateUser(c *gin.Context) {
var req CreateAdminUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
role := normalizeAdminRole(req.Role)
if !isValidAdminRole(role) {
response.Error(c, http.StatusBadRequest, "Invalid role. Must be USER, ADMIN, or BLOCK")
return
}
if err := h.ensurePlanExists(c, req.PlanID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusBadRequest, "Plan not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to create user")
return
}
email := strings.TrimSpace(req.Email)
username := strings.TrimSpace(req.Username)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to hash password")
return
}
password := string(hashedPassword)
user := &model.User{
ID: uuid.New().String(),
Email: email,
Password: &password,
Username: adminStringPtr(username),
Role: &role,
PlanID: nil,
}
if req.PlanID != nil && strings.TrimSpace(*req.PlanID) != "" {
planID := strings.TrimSpace(*req.PlanID)
user.PlanID = &planID
}
if err := h.db.WithContext(c.Request.Context()).Create(user).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
response.Error(c, http.StatusBadRequest, "Email already registered")
return
}
h.logger.Error("Failed to create user", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create user")
return
}
response.Created(c, gin.H{"user": user})
}
// @Summary Get User Detail
// @Description Get detailed info about a single user (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} response.Response
// @Failure 404 {object} response.Response
// @Router /admin/users/{id} [get]
// @Security BearerAuth
func (h *Handler) GetUser(c *gin.Context) {
ctx := c.Request.Context()
id := c.Param("id")
var user model.User
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "User not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to get user")
return
}
var videoCount int64
h.db.WithContext(ctx).Model(&model.Video{}).Where("user_id = ?", id).Count(&videoCount)
balance, _ := model.GetWalletBalance(ctx, h.db, id)
planName := ""
if user.PlanID != nil {
var plan model.Plan
if err := h.db.WithContext(ctx).Where("id = ?", *user.PlanID).First(&plan).Error; err == nil {
planName = plan.Name
}
}
var subscription *model.PlanSubscription
var sub model.PlanSubscription
if err := h.db.WithContext(ctx).Where("user_id = ?", id).Order("created_at DESC").First(&sub).Error; err == nil {
subscription = &sub
}
response.Success(c, gin.H{
"user": gin.H{
"id": user.ID,
"email": user.Email,
"username": user.Username,
"avatar": user.Avatar,
"role": user.Role,
"plan_id": user.PlanID,
"plan_name": planName,
"storage_used": user.StorageUsed,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
},
"video_count": videoCount,
"wallet_balance": balance,
"subscription": subscription,
})
}
// @Summary Update User
// @Description Update a user from admin panel (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Param request body UpdateAdminUserRequest true "User payload"
// @Success 200 {object} response.Response
// @Router /admin/users/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdateUser(c *gin.Context) {
id := c.Param("id")
currentUserID := c.GetString("userID")
var req UpdateAdminUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
updates := map[string]interface{}{}
if req.Email != nil {
email := strings.TrimSpace(*req.Email)
if email == "" {
response.Error(c, http.StatusBadRequest, "Email is required")
return
}
updates["email"] = email
}
if req.Username != nil {
updates["username"] = strings.TrimSpace(*req.Username)
}
if req.Role != nil {
role := normalizeAdminRole(*req.Role)
if !isValidAdminRole(role) {
response.Error(c, http.StatusBadRequest, "Invalid role. Must be USER, ADMIN, or BLOCK")
return
}
if id == currentUserID && role != "ADMIN" {
response.Error(c, http.StatusBadRequest, "Cannot change your own role")
return
}
updates["role"] = role
}
if req.PlanID != nil {
if err := h.ensurePlanExists(c, req.PlanID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusBadRequest, "Plan not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to update user")
return
}
trimmed := strings.TrimSpace(*req.PlanID)
if trimmed == "" {
updates["plan_id"] = nil
} else {
updates["plan_id"] = trimmed
}
}
if req.Password != nil {
if strings.TrimSpace(*req.Password) == "" {
response.Error(c, http.StatusBadRequest, "Password must not be empty")
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to hash password")
return
}
updates["password"] = string(hashedPassword)
}
if len(updates) == 0 {
response.Success(c, gin.H{"message": "No changes provided"})
return
}
result := h.db.WithContext(c.Request.Context()).Model(&model.User{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
response.Error(c, http.StatusBadRequest, "Email already registered")
return
}
h.logger.Error("Failed to update user", "error", result.Error)
response.Error(c, http.StatusInternalServerError, "Failed to update user")
return
}
if result.RowsAffected == 0 {
response.Error(c, http.StatusNotFound, "User not found")
return
}
var user model.User
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&user).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to reload user")
return
}
response.Success(c, gin.H{"user": user})
}
// @Summary Update User Role
// @Description Change user role (admin only). Valid: USER, ADMIN, BLOCK
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Param request body UpdateUserRoleRequest true "Role payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 404 {object} response.Response
// @Router /admin/users/{id}/role [put]
// @Security BearerAuth
func (h *Handler) UpdateUserRole(c *gin.Context) {
id := c.Param("id")
currentUserID := c.GetString("userID")
if id == currentUserID {
response.Error(c, http.StatusBadRequest, "Cannot change your own role")
return
}
var req UpdateUserRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
role := normalizeAdminRole(req.Role)
if !isValidAdminRole(role) {
response.Error(c, http.StatusBadRequest, "Invalid role. Must be USER, ADMIN, or BLOCK")
return
}
result := h.db.WithContext(c.Request.Context()).Model(&model.User{}).Where("id = ?", id).Update("role", role)
if result.Error != nil {
h.logger.Error("Failed to update user role", "error", result.Error)
response.Error(c, http.StatusInternalServerError, "Failed to update role")
return
}
if result.RowsAffected == 0 {
response.Error(c, http.StatusNotFound, "User not found")
return
}
response.Success(c, gin.H{"message": "Role updated", "role": role})
}
// @Summary Delete User
// @Description Delete a user and their data (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 404 {object} response.Response
// @Router /admin/users/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteUser(c *gin.Context) {
id := c.Param("id")
currentUserID := c.GetString("userID")
if id == currentUserID {
response.Error(c, http.StatusBadRequest, "Cannot delete your own account")
return
}
var user model.User
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "User not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to find user")
return
}
err := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
tables := []struct {
model interface{}
where string
}{
{&model.VideoAdConfig{}, "user_id = ?"},
{&model.AdTemplate{}, "user_id = ?"},
{&model.Notification{}, "user_id = ?"},
{&model.Domain{}, "user_id = ?"},
{&model.WalletTransaction{}, "user_id = ?"},
{&model.PlanSubscription{}, "user_id = ?"},
{&model.UserPreference{}, "user_id = ?"},
{&model.Video{}, "user_id = ?"},
{&model.Payment{}, "user_id = ?"},
}
for _, t := range tables {
if err := tx.Where(t.where, id).Delete(t.model).Error; err != nil {
return err
}
}
return tx.Where("id = ?", id).Delete(&model.User{}).Error
})
if err != nil {
h.logger.Error("Failed to delete user", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to delete user")
return
}
response.Success(c, gin.H{"message": "User deleted"})
}

View File

@@ -0,0 +1,477 @@
//go:build ignore
// +build ignore
package admin
import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
type AdminVideoPayload struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
URL string `json:"url"`
Status string `json:"status"`
Size int64 `json:"size"`
Duration int32 `json:"duration"`
Format string `json:"format"`
OwnerEmail string `json:"owner_email,omitempty"`
AdTemplateID *string `json:"ad_template_id,omitempty"`
AdTemplateName string `json:"ad_template_name,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type SaveAdminVideoRequest struct {
UserID string `json:"user_id" binding:"required"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
URL string `json:"url" binding:"required"`
Size int64 `json:"size" binding:"required"`
Duration int32 `json:"duration"`
Format string `json:"format"`
Status string `json:"status"`
AdTemplateID *string `json:"ad_template_id,omitempty"`
}
func normalizeAdminVideoStatus(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "processing", "pending":
return "processing"
case "failed", "error":
return "failed"
default:
return "ready"
}
}
func (h *Handler) loadAdminVideoPayload(ctx *gin.Context, video model.Video) (AdminVideoPayload, error) {
payload := AdminVideoPayload{
ID: video.ID,
UserID: video.UserID,
Title: video.Title,
Description: adminStringValue(video.Description),
URL: video.URL,
Status: adminStringValue(video.Status),
Size: video.Size,
Duration: video.Duration,
Format: video.Format,
CreatedAt: adminFormatTime(video.CreatedAt),
UpdatedAt: adminFormatTimeValue(video.UpdatedAt),
}
var user model.User
if err := h.db.WithContext(ctx.Request.Context()).Select("id, email").Where("id = ?", video.UserID).First(&user).Error; err == nil {
payload.OwnerEmail = user.Email
}
var adConfig model.VideoAdConfig
if err := h.db.WithContext(ctx.Request.Context()).Where("video_id = ?", video.ID).First(&adConfig).Error; err == nil {
payload.AdTemplateID = &adConfig.AdTemplateID
var template model.AdTemplate
if err := h.db.WithContext(ctx.Request.Context()).Where("id = ?", adConfig.AdTemplateID).First(&template).Error; err == nil {
payload.AdTemplateName = template.Name
}
}
return payload, nil
}
func (h *Handler) saveAdminVideoAdConfig(tx *gorm.DB, videoID, userID string, adTemplateID *string) error {
if adTemplateID == nil {
return nil
}
trimmed := strings.TrimSpace(*adTemplateID)
if trimmed == "" {
return tx.Where("video_id = ? AND user_id = ?", videoID, userID).Delete(&model.VideoAdConfig{}).Error
}
var template model.AdTemplate
if err := tx.Where("id = ? AND user_id = ?", trimmed, userID).First(&template).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &gin.Error{Err: errors.New("Ad template not found"), Type: gin.ErrorTypeBind}
}
return err
}
var existing model.VideoAdConfig
if err := tx.Where("video_id = ? AND user_id = ?", videoID, userID).First(&existing).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return tx.Create(&model.VideoAdConfig{
VideoID: videoID,
UserID: userID,
AdTemplateID: template.ID,
VastTagURL: template.VastTagURL,
AdFormat: template.AdFormat,
Duration: template.Duration,
}).Error
}
return err
}
existing.AdTemplateID = template.ID
existing.VastTagURL = template.VastTagURL
existing.AdFormat = template.AdFormat
existing.Duration = template.Duration
return tx.Save(&existing).Error
}
// @Summary List All Videos
// @Description Get paginated list of all videos across users (admin only)
// @Tags admin
// @Produce json
// @Param page query int false "Page" default(1)
// @Param limit query int false "Limit" default(20)
// @Param search query string false "Search by title"
// @Param user_id query string false "Filter by user ID"
// @Param status query string false "Filter by status"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Router /admin/videos [get]
// @Security BearerAuth
func (h *Handler) ListVideos(c *gin.Context) {
ctx := c.Request.Context()
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
offset := (page - 1) * limit
search := strings.TrimSpace(c.Query("search"))
userID := strings.TrimSpace(c.Query("user_id"))
status := strings.TrimSpace(c.Query("status"))
db := h.db.WithContext(ctx).Model(&model.Video{})
if search != "" {
like := "%" + search + "%"
db = db.Where("title ILIKE ?", like)
}
if userID != "" {
db = db.Where("user_id = ?", userID)
}
if status != "" && !strings.EqualFold(status, "all") {
db = db.Where("status = ?", normalizeAdminVideoStatus(status))
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to list videos")
return
}
var videos []model.Video
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&videos).Error; err != nil {
h.logger.Error("Failed to list videos", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to list videos")
return
}
result := make([]AdminVideoPayload, 0, len(videos))
for _, v := range videos {
payload, err := h.loadAdminVideoPayload(c, v)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to list videos")
return
}
result = append(result, payload)
}
response.Success(c, gin.H{
"videos": result,
"total": total,
"page": page,
"limit": limit,
})
}
// @Summary Get Video Detail
// @Description Get video detail by ID (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "Video ID"
// @Success 200 {object} response.Response
// @Router /admin/videos/{id} [get]
// @Security BearerAuth
func (h *Handler) GetVideo(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
var video model.Video
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&video).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to get video")
return
}
payload, err := h.loadAdminVideoPayload(c, video)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to get video")
return
}
response.Success(c, gin.H{"video": payload})
}
// @Summary Create Video
// @Description Create a model video record for a user (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param request body SaveAdminVideoRequest true "Video payload"
// @Success 201 {object} response.Response
// @Router /admin/videos [post]
// @Security BearerAuth
func (h *Handler) CreateVideo(c *gin.Context) {
var req SaveAdminVideoRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if strings.TrimSpace(req.UserID) == "" || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.URL) == "" {
response.Error(c, http.StatusBadRequest, "User ID, title, and URL are required")
return
}
if req.Size < 0 {
response.Error(c, http.StatusBadRequest, "Size must be greater than or equal to 0")
return
}
ctx := c.Request.Context()
var user model.User
if err := h.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.UserID)).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusBadRequest, "User not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to create video")
return
}
status := normalizeAdminVideoStatus(req.Status)
processingStatus := strings.ToUpper(status)
storageType := "WORKER"
video := &model.Video{
ID: uuid.New().String(),
UserID: user.ID,
Name: strings.TrimSpace(req.Title),
Title: strings.TrimSpace(req.Title),
Description: adminStringPtr(req.Description),
URL: strings.TrimSpace(req.URL),
Size: req.Size,
Duration: req.Duration,
Format: strings.TrimSpace(req.Format),
Status: &status,
ProcessingStatus: &processingStatus,
StorageType: &storageType,
}
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(video).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
return err
}
if err := h.saveAdminVideoAdConfig(tx, video.ID, user.ID, req.AdTemplateID); err != nil {
return err
}
return nil
}); err != nil {
if strings.Contains(err.Error(), "Ad template not found") {
response.Error(c, http.StatusBadRequest, "Ad template not found")
return
}
h.logger.Error("Failed to create video", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create video")
return
}
payload, err := h.loadAdminVideoPayload(c, *video)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to create video")
return
}
response.Created(c, gin.H{"video": payload})
}
// @Summary Update Video
// @Description Update video metadata and status (admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "Video ID"
// @Param request body SaveAdminVideoRequest true "Video payload"
// @Success 200 {object} response.Response
// @Router /admin/videos/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdateVideo(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
var req SaveAdminVideoRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
if strings.TrimSpace(req.UserID) == "" || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.URL) == "" {
response.Error(c, http.StatusBadRequest, "User ID, title, and URL are required")
return
}
if req.Size < 0 {
response.Error(c, http.StatusBadRequest, "Size must be greater than or equal to 0")
return
}
ctx := c.Request.Context()
var video model.Video
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to update video")
return
}
var user model.User
if err := h.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.UserID)).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusBadRequest, "User not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to update video")
return
}
oldSize := video.Size
oldUserID := video.UserID
status := normalizeAdminVideoStatus(req.Status)
processingStatus := strings.ToUpper(status)
video.UserID = user.ID
video.Name = strings.TrimSpace(req.Title)
video.Title = strings.TrimSpace(req.Title)
video.Description = adminStringPtr(req.Description)
video.URL = strings.TrimSpace(req.URL)
video.Size = req.Size
video.Duration = req.Duration
video.Format = strings.TrimSpace(req.Format)
video.Status = &status
video.ProcessingStatus = &processingStatus
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&video).Error; err != nil {
return err
}
if oldUserID == user.ID {
delta := video.Size - oldSize
if delta != 0 {
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used + ?, 0)", delta)).Error; err != nil {
return err
}
}
} else {
if err := tx.Model(&model.User{}).Where("id = ?", oldUserID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", oldSize)).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
return err
}
}
if oldUserID != user.ID {
if err := tx.Model(&model.VideoAdConfig{}).Where("video_id = ?", video.ID).Update("user_id", user.ID).Error; err != nil {
return err
}
}
if err := h.saveAdminVideoAdConfig(tx, video.ID, user.ID, req.AdTemplateID); err != nil {
return err
}
return nil
}); err != nil {
if strings.Contains(err.Error(), "Ad template not found") {
response.Error(c, http.StatusBadRequest, "Ad template not found")
return
}
h.logger.Error("Failed to update video", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update video")
return
}
payload, err := h.loadAdminVideoPayload(c, video)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to update video")
return
}
response.Success(c, gin.H{"video": payload})
}
// @Summary Delete Video (Admin)
// @Description Delete any video by ID (admin only)
// @Tags admin
// @Produce json
// @Param id path string true "Video ID"
// @Success 200 {object} response.Response
// @Failure 404 {object} response.Response
// @Router /admin/videos/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteVideo(c *gin.Context) {
id := c.Param("id")
var video model.Video
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&video).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
response.Error(c, http.StatusInternalServerError, "Failed to find video")
return
}
err := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("video_id = ?", video.ID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("id = ?", video.ID).Delete(&model.Video{}).Error; err != nil {
return err
}
return tx.Model(&model.User{}).Where("id = ?", video.UserID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", video.Size)).Error
})
if err != nil {
h.logger.Error("Failed to delete video", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to delete video")
return
}
response.Success(c, gin.H{"message": "Video deleted"})
}

View File

@@ -0,0 +1,338 @@
//go:build ignore
// +build ignore
package adtemplates
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/logger"
"stream.api/pkg/response"
)
const upgradeRequiredMessage = "Upgrade required to manage Ads & VAST"
type Handler struct {
logger logger.Logger
db *gorm.DB
}
type SaveAdTemplateRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
VASTTagURL string `json:"vast_tag_url" binding:"required"`
AdFormat string `json:"ad_format"`
Duration *int `json:"duration"`
IsActive *bool `json:"is_active"`
IsDefault *bool `json:"is_default"`
}
type TemplatePayload struct {
Template *model.AdTemplate `json:"template"`
}
type TemplateListPayload struct {
Templates []model.AdTemplate `json:"templates"`
}
func NewHandler(l logger.Logger, db *gorm.DB) *Handler {
return &Handler{logger: l, db: db}
}
// @Summary List Ad Templates
// @Description Get all VAST ad templates for the current user
// @Tags ad-templates
// @Produce json
// @Success 200 {object} response.Response{data=TemplateListPayload}
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates [get]
// @Security BearerAuth
func (h *Handler) ListTemplates(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var items []model.AdTemplate
if err := h.db.WithContext(c.Request.Context()).
Where("user_id = ?", userID).
Order("is_default DESC").
Order("created_at DESC").
Find(&items).Error; err != nil {
h.logger.Error("Failed to list ad templates", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to load ad templates")
return
}
response.Success(c, gin.H{"templates": items})
}
// @Summary Create Ad Template
// @Description Create a VAST ad template for the current user
// @Tags ad-templates
// @Accept json
// @Produce json
// @Param request body SaveAdTemplateRequest true "Ad template payload"
// @Success 201 {object} response.Response{data=TemplatePayload}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates [post]
// @Security BearerAuth
func (h *Handler) CreateTemplate(c *gin.Context) {
h.saveTemplate(c, true)
}
// @Summary Update Ad Template
// @Description Update a VAST ad template for the current user
// @Tags ad-templates
// @Accept json
// @Produce json
// @Param id path string true "Ad Template ID"
// @Param request body SaveAdTemplateRequest true "Ad template payload"
// @Success 200 {object} response.Response{data=TemplatePayload}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdateTemplate(c *gin.Context) {
h.saveTemplate(c, false)
}
// @Summary Delete Ad Template
// @Description Delete a VAST ad template for the current user
// @Tags ad-templates
// @Produce json
// @Param id path string true "Ad Template ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteTemplate(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if !requirePaidPlan(c) {
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
result := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("ad_template_id = ? AND user_id = ?", id, userID).
Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
res := tx.Where("id = ? AND user_id = ?", id, userID).Delete(&model.AdTemplate{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
if result != nil {
if result == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
h.logger.Error("Failed to delete ad template", "error", result)
response.Error(c, http.StatusInternalServerError, "Failed to delete ad template")
return
}
response.Success(c, gin.H{"message": "Ad template deleted"})
}
func (h *Handler) saveTemplate(c *gin.Context, create bool) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if !requirePaidPlan(c) {
return
}
var req SaveAdTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
name := strings.TrimSpace(req.Name)
vastURL := strings.TrimSpace(req.VASTTagURL)
if name == "" || vastURL == "" {
response.Error(c, http.StatusBadRequest, "Name and VAST URL are required")
return
}
format := normalizeAdFormat(req.AdFormat)
if format == "mid-roll" && (req.Duration == nil || *req.Duration <= 0) {
response.Error(c, http.StatusBadRequest, "Duration is required for mid-roll templates")
return
}
ctx := c.Request.Context()
if create {
item := &model.AdTemplate{
ID: uuid.New().String(),
UserID: userID,
Name: name,
Description: stringPointer(strings.TrimSpace(req.Description)),
VastTagURL: vastURL,
AdFormat: model.StringPtr(format),
Duration: intPtrToInt64Ptr(req.Duration),
IsActive: model.BoolPtr(req.IsActive == nil || *req.IsActive),
IsDefault: req.IsDefault != nil && *req.IsDefault,
}
if !adTemplateIsActive(item.IsActive) {
item.IsDefault = false
}
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetDefaultTemplates(tx, userID, ""); err != nil {
return err
}
}
return tx.Create(item).Error
}); err != nil {
h.logger.Error("Failed to create ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
response.Created(c, gin.H{"template": item})
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
var item model.AdTemplate
if err := h.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&item).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
h.logger.Error("Failed to load ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
item.Name = name
item.Description = stringPointer(strings.TrimSpace(req.Description))
item.VastTagURL = vastURL
item.AdFormat = model.StringPtr(format)
item.Duration = intPtrToInt64Ptr(req.Duration)
if req.IsActive != nil {
item.IsActive = model.BoolPtr(*req.IsActive)
}
if req.IsDefault != nil {
item.IsDefault = *req.IsDefault
}
if !adTemplateIsActive(item.IsActive) {
item.IsDefault = false
}
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetDefaultTemplates(tx, userID, item.ID); err != nil {
return err
}
}
return tx.Save(&item).Error
}); err != nil {
h.logger.Error("Failed to update ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
response.Success(c, gin.H{"template": item})
}
func requirePaidPlan(c *gin.Context) bool {
userValue, exists := c.Get("user")
if !exists {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return false
}
user, ok := userValue.(*model.User)
if !ok || user == nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return false
}
if user.PlanID == nil || strings.TrimSpace(*user.PlanID) == "" {
response.Error(c, http.StatusForbidden, upgradeRequiredMessage)
return false
}
return true
}
func unsetDefaultTemplates(tx *gorm.DB, userID, excludeID string) error {
query := tx.Model(&model.AdTemplate{}).Where("user_id = ?", userID)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
return query.Update("is_default", false).Error
}
func normalizeAdFormat(value string) string {
switch strings.TrimSpace(strings.ToLower(value)) {
case "mid-roll", "post-roll":
return strings.TrimSpace(strings.ToLower(value))
default:
return "pre-roll"
}
}
func stringPointer(value string) *string {
if value == "" {
return nil
}
return &value
}
func intPtrToInt64Ptr(value *int) *int64 {
if value == nil {
return nil
}
converted := int64(*value)
return &converted
}
func adTemplateIsActive(value *bool) bool {
return value == nil || *value
}

View File

@@ -1,8 +1,17 @@
//go:build ignore
// +build ignore
package auth package auth
import ( import (
"crypto/rand"
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt"
"net/http" "net/http"
"net/url"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -10,6 +19,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
"gorm.io/gorm"
"stream.api/internal/config" "stream.api/internal/config"
"stream.api/internal/database/model" "stream.api/internal/database/model"
"stream.api/internal/database/query" "stream.api/internal/database/query"
@@ -20,18 +30,31 @@ import (
) )
type handler struct { type handler struct {
cache cache.Cache cache cache.Cache
token token.Provider token token.Provider
logger logger.Logger logger logger.Logger
googleOauth *oauth2.Config db *gorm.DB
googleOauth *oauth2.Config
googleStateTTL time.Duration
frontendBaseURL string
googleFinalizePath string
} }
// NewHandler creates a new instance of Handler // NewHandler creates a new instance of Handler
func NewHandler(c cache.Cache, t token.Provider, l logger.Logger, cfg *config.Config) AuthHandler { func NewHandler(c cache.Cache, t token.Provider, l logger.Logger, cfg *config.Config, db *gorm.DB) AuthHandler {
stateTTL := time.Duration(cfg.Google.StateTTLMinute) * time.Minute
if stateTTL <= 0 {
stateTTL = 10 * time.Minute
}
return &handler{ return &handler{
cache: c, cache: c,
token: t, token: t,
logger: l, logger: l,
db: db,
googleStateTTL: stateTTL,
frontendBaseURL: strings.TrimRight(cfg.Frontend.BaseURL, "/"),
googleFinalizePath: cfg.Frontend.GoogleAuthFinalizePath,
googleOauth: &oauth2.Config{ googleOauth: &oauth2.Config{
ClientID: cfg.Google.ClientID, ClientID: cfg.Google.ClientID,
ClientSecret: cfg.Google.ClientSecret, ClientSecret: cfg.Google.ClientSecret,
@@ -45,6 +68,16 @@ func NewHandler(c cache.Cache, t token.Provider, l logger.Logger, cfg *config.Co
} }
} }
// @Summary Login
// @Description Login with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body LoginRequest true "Login payload"
// @Success 200 {object} response.Response{data=UserPayload}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Router /auth/login [post]
func (h *handler) Login(c *gin.Context) { func (h *handler) Login(c *gin.Context) {
var req LoginRequest var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -70,10 +103,20 @@ func (h *handler) Login(c *gin.Context) {
return return
} }
h.generateAndSetTokens(c, user.ID, user.Email, *user.Role) if err := h.generateAndSetTokens(c, user.ID, user.Email, safeRole(user.Role)); err != nil {
response.Success(c, gin.H{"user": user}) return
}
h.respondWithUserPayload(c, user)
} }
// @Summary Logout
// @Description Logout user and clear cookies
// @Tags auth
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Router /auth/logout [post]
// @Security BearerAuth
func (h *handler) Logout(c *gin.Context) { func (h *handler) Logout(c *gin.Context) {
refreshToken, err := c.Cookie("refresh_token") refreshToken, err := c.Cookie("refresh_token")
if err == nil { if err == nil {
@@ -90,6 +133,16 @@ func (h *handler) Logout(c *gin.Context) {
response.Success(c, "Logged out") response.Success(c, "Logged out")
} }
// @Summary Register
// @Description Register a new user
// @Tags auth
// @Accept json
// @Produce json
// @Param request body RegisterRequest true "Registration payload"
// @Success 201 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /auth/register [post]
func (h *handler) Register(c *gin.Context) { func (h *handler) Register(c *gin.Context) {
var req RegisterRequest var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -129,6 +182,16 @@ func (h *handler) Register(c *gin.Context) {
response.Created(c, "User registered") response.Created(c, "User registered")
} }
// @Summary Forgot Password
// @Description Request password reset link
// @Tags auth
// @Accept json
// @Produce json
// @Param request body ForgotPasswordRequest true "Forgot password payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /auth/forgot-password [post]
func (h *handler) ForgotPassword(c *gin.Context) { func (h *handler) ForgotPassword(c *gin.Context) {
var req ForgotPasswordRequest var req ForgotPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -157,6 +220,16 @@ func (h *handler) ForgotPassword(c *gin.Context) {
response.Success(c, gin.H{"message": "If email exists, a reset link has been sent", "debug_token": tokenID}) response.Success(c, gin.H{"message": "If email exists, a reset link has been sent", "debug_token": tokenID})
} }
// @Summary Reset Password
// @Description Reset password using token
// @Tags auth
// @Accept json
// @Produce json
// @Param request body ResetPasswordRequest true "Reset password payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /auth/reset-password [post]
func (h *handler) ResetPassword(c *gin.Context) { func (h *handler) ResetPassword(c *gin.Context) {
var req ResetPasswordRequest var req ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -193,27 +266,82 @@ func (h *handler) ResetPassword(c *gin.Context) {
response.Success(c, "Password reset successfully") response.Success(c, "Password reset successfully")
} }
// @Summary Google Login
// @Description Redirect to Google for Login
// @Tags auth
// @Router /auth/google/login [get]
func (h *handler) LoginGoogle(c *gin.Context) { func (h *handler) LoginGoogle(c *gin.Context) {
url := h.googleOauth.AuthCodeURL("state", oauth2.AccessTypeOffline) state, err := generateOAuthState()
if err != nil {
h.logger.Error("Failed to generate Google OAuth state", "error", err)
response.Fail(c, "Failed to start Google login")
return
}
if err := h.cache.Set(c.Request.Context(), googleOAuthStateCacheKey(state), "1", h.googleStateTTL); err != nil {
h.logger.Error("Failed to persist Google OAuth state", "error", err)
response.Fail(c, "Failed to start Google login")
return
}
url := h.googleOauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
} }
// @Summary Google Callback
// @Description Callback for Google Login
// @Tags auth
// @Success 307
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /auth/google/callback [get]
func (h *handler) GoogleCallback(c *gin.Context) { func (h *handler) GoogleCallback(c *gin.Context) {
code := c.Query("code") if oauthError := strings.TrimSpace(c.Query("error")); oauthError != "" {
h.redirectToGoogleFinalize(c, "error", oauthError)
return
}
state := strings.TrimSpace(c.Query("state"))
if state == "" {
h.redirectToGoogleFinalize(c, "error", "missing_state")
return
}
cachedState, err := h.cache.Get(c.Request.Context(), googleOAuthStateCacheKey(state))
if err != nil || cachedState == "" {
h.redirectToGoogleFinalize(c, "error", "invalid_state")
return
}
_ = h.cache.Del(c.Request.Context(), googleOAuthStateCacheKey(state))
code := strings.TrimSpace(c.Query("code"))
if code == "" {
h.redirectToGoogleFinalize(c, "error", "missing_code")
return
}
tokenResp, err := h.googleOauth.Exchange(c.Request.Context(), code) tokenResp, err := h.googleOauth.Exchange(c.Request.Context(), code)
if err != nil { if err != nil {
response.Error(c, http.StatusBadRequest, "Failed to exchange token") h.logger.Error("Failed to exchange Google OAuth token", "error", err)
h.redirectToGoogleFinalize(c, "error", "exchange_failed")
return return
} }
client := h.googleOauth.Client(c.Request.Context(), tokenResp) client := h.googleOauth.Client(c.Request.Context(), tokenResp)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil || resp.StatusCode != http.StatusOK { if err != nil {
response.Fail(c, "Failed to get user info") h.logger.Error("Failed to fetch Google user info", "error", err)
h.redirectToGoogleFinalize(c, "error", "userinfo_failed")
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
h.logger.Error("Google user info returned non-200", "status", resp.StatusCode)
h.redirectToGoogleFinalize(c, "error", "userinfo_failed")
return
}
var googleUser struct { var googleUser struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
@@ -221,7 +349,13 @@ func (h *handler) GoogleCallback(c *gin.Context) {
Picture string `json:"picture"` Picture string `json:"picture"`
} }
if err := json.NewDecoder(resp.Body).Decode(&googleUser); err != nil { if err := json.NewDecoder(resp.Body).Decode(&googleUser); err != nil {
response.Fail(c, "Failed to parse user info") h.logger.Error("Failed to decode Google user info", "error", err)
h.redirectToGoogleFinalize(c, "error", "userinfo_parse_failed")
return
}
if strings.TrimSpace(googleUser.Email) == "" {
h.redirectToGoogleFinalize(c, "error", "missing_email")
return return
} }
@@ -232,29 +366,300 @@ func (h *handler) GoogleCallback(c *gin.Context) {
user = &model.User{ user = &model.User{
ID: uuid.New().String(), ID: uuid.New().String(),
Email: googleUser.Email, Email: googleUser.Email,
Username: &googleUser.Name, Username: stringPointerOrNil(googleUser.Name),
GoogleID: &googleUser.ID, GoogleID: stringPointerOrNil(googleUser.ID),
Avatar: &googleUser.Picture, Avatar: stringPointerOrNil(googleUser.Picture),
Role: &role, Role: &role,
} }
if err := u.WithContext(c.Request.Context()).Create(user); err != nil { if err := u.WithContext(c.Request.Context()).Create(user); err != nil {
response.Fail(c, "Failed to create user") h.logger.Error("Failed to create Google user", "error", err)
h.redirectToGoogleFinalize(c, "error", "create_user_failed")
return return
} }
} else if user.GoogleID == nil || *user.GoogleID == "" { } else {
u.WithContext(c.Request.Context()).Where(u.ID.Eq(user.ID)).Update(u.GoogleID, googleUser.ID) updates := map[string]interface{}{}
if user.GoogleID == nil || strings.TrimSpace(*user.GoogleID) == "" {
updates["google_id"] = googleUser.ID
}
if user.Avatar == nil || strings.TrimSpace(*user.Avatar) == "" {
updates["avatar"] = googleUser.Picture
}
if user.Username == nil || strings.TrimSpace(*user.Username) == "" {
updates["username"] = googleUser.Name
}
if len(updates) > 0 {
if err := h.db.WithContext(c.Request.Context()).Model(&model.User{}).Where("id = ?", user.ID).Updates(updates).Error; err != nil {
h.logger.Error("Failed to update Google user", "error", err)
h.redirectToGoogleFinalize(c, "error", "update_user_failed")
return
}
user, err = u.WithContext(c.Request.Context()).Where(u.ID.Eq(user.ID)).First()
if err != nil {
h.logger.Error("Failed to reload Google user", "error", err)
h.redirectToGoogleFinalize(c, "error", "reload_user_failed")
return
}
}
} }
h.generateAndSetTokens(c, user.ID, user.Email, *user.Role) if err := h.generateAndSetTokens(c, user.ID, user.Email, safeRole(user.Role)); err != nil {
response.Success(c, gin.H{"user": user}) h.redirectToGoogleFinalize(c, "error", "session_failed")
return
}
if h.frontendBaseURL == "" {
h.respondWithUserPayload(c, user)
return
}
h.redirectToGoogleFinalize(c, "success", "")
} }
func (h *handler) generateAndSetTokens(c *gin.Context, userID, email, role string) { // @Summary Get Current User
// @Description Get the authenticated user's profile payload
// @Tags auth
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Router /me [get]
// @Security BearerAuth
func (h *handler) GetMe(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
u := query.User
user, err := u.WithContext(c.Request.Context()).Where(u.ID.Eq(userID)).First()
if err != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
h.respondWithUserPayload(c, user)
}
// @Summary Update Current User
// @Description Update the authenticated user's profile information
// @Tags auth
// @Accept json
// @Produce json
// @Param request body UpdateMeRequest true "Profile payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /me [put]
// @Security BearerAuth
func (h *handler) UpdateMe(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var req UpdateMeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
user, err := UpdateUserProfile(c.Request.Context(), h.db, h.logger, userID, UpdateProfileInput{
Username: req.Username,
Email: req.Email,
Language: req.Language,
Locale: req.Locale,
})
if err != nil {
switch {
case errors.Is(err, ErrEmailRequired):
response.Error(c, http.StatusBadRequest, err.Error())
case errors.Is(err, ErrEmailAlreadyRegistered):
response.Error(c, http.StatusBadRequest, err.Error())
default:
response.Fail(c, "Failed to update profile")
}
return
}
h.respondWithUserPayload(c, user)
}
// @Summary Change Password
// @Description Change the authenticated user's local password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body ChangePasswordRequest true "Password payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /auth/change-password [post]
// @Security BearerAuth
func (h *handler) ChangePassword(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
u := query.User
user, err := u.WithContext(c.Request.Context()).Where(u.ID.Eq(userID)).First()
if err != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if user.Password == nil || strings.TrimSpace(*user.Password) == "" {
response.Error(c, http.StatusBadRequest, "This account does not have a local password")
return
}
if err := bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(req.CurrentPassword)); err != nil {
response.Error(c, http.StatusBadRequest, "Current password is incorrect")
return
}
if req.CurrentPassword == req.NewPassword {
response.Error(c, http.StatusBadRequest, "New password must be different")
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
response.Fail(c, "Failed to hash password")
return
}
if _, err := u.WithContext(c.Request.Context()).Where(u.ID.Eq(userID)).Update(u.Password, string(hashedPassword)); err != nil {
h.logger.Error("Failed to change password", "error", err)
response.Fail(c, "Failed to change password")
return
}
response.Success(c, gin.H{"message": "Password changed successfully"})
}
// @Summary Clear My Data
// @Description Remove videos and settings-related resources for the authenticated user
// @Tags auth
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /me/clear-data [post]
// @Security BearerAuth
func (h *handler) ClearMyData(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
ctx := c.Request.Context()
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&model.Notification{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Domain{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.AdTemplate{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Video{}).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{"storage_used": 0}).Error; err != nil {
return err
}
return nil
}); err != nil {
h.logger.Error("Failed to clear user data", "error", err)
response.Fail(c, "Failed to clear data")
return
}
response.Success(c, gin.H{"message": "Data cleared successfully"})
}
// @Summary Delete My Account
// @Description Permanently delete the authenticated user's account and related data
// @Tags auth
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /me [delete]
// @Security BearerAuth
func (h *handler) DeleteMe(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
ctx := c.Request.Context()
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&model.Notification{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Domain{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.AdTemplate{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.WalletTransaction{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.PlanSubscription{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.UserPreference{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Payment{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Video{}).Error; err != nil {
return err
}
if err := tx.Where("id = ?", userID).Delete(&model.User{}).Error; err != nil {
return err
}
return nil
}); err != nil {
h.logger.Error("Failed to delete user", "error", err)
response.Fail(c, "Failed to delete account")
return
}
c.SetCookie("access_token", "", -1, "/", "", false, true)
c.SetCookie("refresh_token", "", -1, "/", "", false, true)
response.Success(c, gin.H{"message": "Account deleted successfully"})
}
func (h *handler) generateAndSetTokens(c *gin.Context, userID, email, role string) error {
td, err := h.token.GenerateTokenPair(userID, email, role) td, err := h.token.GenerateTokenPair(userID, email, role)
if err != nil { if err != nil {
h.logger.Error("Token generation failed", "error", err) h.logger.Error("Token generation failed", "error", err)
response.Fail(c, "Error generating tokens") response.Fail(c, "Error generating tokens")
return return err
} }
// Store Refresh UUID in Redis // Store Refresh UUID in Redis
@@ -262,9 +667,79 @@ func (h *handler) generateAndSetTokens(c *gin.Context, userID, email, role strin
if err != nil { if err != nil {
h.logger.Error("Session storage failed", "error", err) h.logger.Error("Session storage failed", "error", err)
response.Fail(c, "Error storing session") response.Fail(c, "Error storing session")
return return err
} }
c.SetCookie("access_token", td.AccessToken, int(td.AtExpires-time.Now().Unix()), "/", "", false, true) c.SetCookie("access_token", td.AccessToken, int(td.AtExpires-time.Now().Unix()), "/", "", false, true)
c.SetCookie("refresh_token", td.RefreshToken, int(td.RtExpires-time.Now().Unix()), "/", "", false, true) c.SetCookie("refresh_token", td.RefreshToken, int(td.RtExpires-time.Now().Unix()), "/", "", false, true)
return nil
}
func (h *handler) respondWithUserPayload(c *gin.Context, user *model.User) {
payload, err := BuildUserPayload(c.Request.Context(), h.db, user)
if err != nil {
h.logger.Error("Failed to build user payload", "error", err)
response.Fail(c, "Failed to build user payload")
return
}
response.Success(c, gin.H{"user": payload})
}
func safeRole(role *string) string {
if role == nil || strings.TrimSpace(*role) == "" {
return "USER"
}
return *role
}
func generateOAuthState() (string, error) {
buffer := make([]byte, 32)
if _, err := rand.Read(buffer); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buffer), nil
}
func googleOAuthStateCacheKey(state string) string {
return "google_oauth_state:" + state
}
func stringPointerOrNil(value string) *string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
return &trimmed
}
func (h *handler) redirectToGoogleFinalize(c *gin.Context, status, reason string) {
finalizeURL := h.googleFinalizeURL(status, reason)
if finalizeURL == "" {
response.Error(c, http.StatusBadRequest, reason)
return
}
c.Redirect(http.StatusTemporaryRedirect, finalizeURL)
}
func (h *handler) googleFinalizeURL(status, reason string) string {
if h.frontendBaseURL == "" {
return ""
}
finalizePath := h.googleFinalizePath
if strings.TrimSpace(finalizePath) == "" {
finalizePath = "/auth/google/finalize"
}
if !strings.HasPrefix(finalizePath, "/") {
finalizePath = "/" + finalizePath
}
values := url.Values{}
values.Set("status", status)
if strings.TrimSpace(reason) != "" {
values.Set("reason", reason)
}
return fmt.Sprintf("%s%s?%s", h.frontendBaseURL, finalizePath, values.Encode())
} }

View File

@@ -1,3 +1,6 @@
//go:build ignore
// +build ignore
package auth package auth
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
@@ -11,6 +14,11 @@ type AuthHandler interface {
ResetPassword(c *gin.Context) ResetPassword(c *gin.Context)
LoginGoogle(c *gin.Context) LoginGoogle(c *gin.Context)
GoogleCallback(c *gin.Context) GoogleCallback(c *gin.Context)
GetMe(c *gin.Context)
UpdateMe(c *gin.Context)
ChangePassword(c *gin.Context)
DeleteMe(c *gin.Context)
ClearMyData(c *gin.Context)
} }
// LoginRequest defines the payload for login // LoginRequest defines the payload for login
@@ -36,3 +44,15 @@ type ResetPasswordRequest struct {
Token string `json:"token" binding:"required"` Token string `json:"token" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"` NewPassword string `json:"new_password" binding:"required,min=6"`
} }
type UpdateMeRequest struct {
Username *string `json:"username"`
Email *string `json:"email"`
Language *string `json:"language"`
Locale *string `json:"locale"`
}
type ChangePasswordRequest struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}

View File

@@ -0,0 +1,87 @@
package auth
import (
"context"
"errors"
"strings"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
"stream.api/pkg/logger"
)
var (
ErrEmailRequired = errors.New("Email is required")
ErrEmailAlreadyRegistered = errors.New("Email already registered")
)
type UpdateProfileInput struct {
Username *string
Email *string
Language *string
Locale *string
}
func UpdateUserProfile(ctx context.Context, db *gorm.DB, l logger.Logger, userID string, req UpdateProfileInput) (*model.User, error) {
updates := map[string]any{}
if req.Username != nil {
username := strings.TrimSpace(*req.Username)
updates["username"] = username
}
if req.Email != nil {
email := strings.TrimSpace(*req.Email)
if email == "" {
return nil, ErrEmailRequired
}
updates["email"] = email
}
if len(updates) > 0 {
if err := db.WithContext(ctx).Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, ErrEmailAlreadyRegistered
}
l.Error("Failed to update user", "error", err)
return nil, err
}
}
pref, err := model.FindOrCreateUserPreference(ctx, db, userID)
if err != nil {
l.Error("Failed to load user preference", "error", err)
return nil, err
}
prefChanged := false
if req.Language != nil {
pref.Language = model.StringPtr(strings.TrimSpace(*req.Language))
prefChanged = true
}
if req.Locale != nil {
pref.Locale = model.StringPtr(strings.TrimSpace(*req.Locale))
prefChanged = true
}
if strings.TrimSpace(model.StringValue(pref.Language)) == "" {
pref.Language = model.StringPtr("en")
prefChanged = true
}
if strings.TrimSpace(model.StringValue(pref.Locale)) == "" {
pref.Locale = model.StringPtr(model.StringValue(pref.Language))
prefChanged = true
}
if prefChanged {
if err := db.WithContext(ctx).Save(pref).Error; err != nil {
l.Error("Failed to save user preference", "error", err)
return nil, err
}
}
u := query.User
user, err := u.WithContext(ctx).Where(u.ID.Eq(userID)).First()
if err != nil {
return nil, err
}
return user, nil
}

View File

@@ -0,0 +1,33 @@
package auth
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type RegisterRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Username string `json:"username" binding:"required"`
}
type ForgotPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
}
type ResetPasswordRequest struct {
Token string `json:"token" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
type UpdateMeRequest struct {
Username *string `json:"username"`
Email *string `json:"email"`
Language *string `json:"language"`
Locale *string `json:"locale"`
}
type ChangePasswordRequest struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}

View File

@@ -0,0 +1,114 @@
package auth
import (
"context"
"errors"
"strings"
"time"
"gorm.io/gorm"
"stream.api/internal/database/model"
)
type UserPayload struct {
ID string `json:"id"`
Email string `json:"email"`
Username *string `json:"username,omitempty"`
Avatar *string `json:"avatar,omitempty"`
Role *string `json:"role,omitempty"`
GoogleID *string `json:"google_id,omitempty"`
StorageUsed int64 `json:"storage_used"`
PlanID *string `json:"plan_id,omitempty"`
PlanStartedAt *time.Time `json:"plan_started_at,omitempty"`
PlanExpiresAt *time.Time `json:"plan_expires_at,omitempty"`
PlanTermMonths *int32 `json:"plan_term_months,omitempty"`
PlanPaymentMethod *string `json:"plan_payment_method,omitempty"`
PlanExpiringSoon bool `json:"plan_expiring_soon"`
WalletBalance float64 `json:"wallet_balance"`
Language string `json:"language"`
Locale string `json:"locale"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
func BuildUserPayload(ctx context.Context, db *gorm.DB, user *model.User) (*UserPayload, error) {
pref, err := model.FindOrCreateUserPreference(ctx, db, user.ID)
if err != nil {
return nil, err
}
walletBalance, err := model.GetWalletBalance(ctx, db, user.ID)
if err != nil {
return nil, err
}
language := strings.TrimSpace(model.StringValue(pref.Language))
if language == "" {
language = "en"
}
locale := strings.TrimSpace(model.StringValue(pref.Locale))
if locale == "" {
locale = language
}
effectivePlanID := user.PlanID
var planStartedAt *time.Time
var planExpiresAt *time.Time
var planTermMonths *int32
var planPaymentMethod *string
planExpiringSoon := false
now := time.Now().UTC()
subscription, err := model.GetLatestPlanSubscription(ctx, db, user.ID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if err == nil {
startedAt := subscription.StartedAt.UTC()
expiresAt := subscription.ExpiresAt.UTC()
termMonths := subscription.TermMonths
paymentMethod := normalizePlanPaymentMethod(subscription.PaymentMethod)
planStartedAt = &startedAt
planExpiresAt = &expiresAt
planTermMonths = &termMonths
planPaymentMethod = &paymentMethod
if expiresAt.After(now) {
effectivePlanID = &subscription.PlanID
planExpiringSoon = model.IsSubscriptionExpiringSoon(expiresAt, now)
} else {
effectivePlanID = nil
}
}
return &UserPayload{
ID: user.ID,
Email: user.Email,
Username: user.Username,
Avatar: user.Avatar,
Role: user.Role,
GoogleID: user.GoogleID,
StorageUsed: user.StorageUsed,
PlanID: effectivePlanID,
PlanStartedAt: planStartedAt,
PlanExpiresAt: planExpiresAt,
PlanTermMonths: planTermMonths,
PlanPaymentMethod: planPaymentMethod,
PlanExpiringSoon: planExpiringSoon,
WalletBalance: walletBalance,
Language: language,
Locale: locale,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}, nil
}
func normalizePlanPaymentMethod(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "topup":
return "topup"
default:
return "wallet"
}
}

View File

@@ -0,0 +1,166 @@
//go:build ignore
// +build ignore
package domains
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/logger"
"stream.api/pkg/response"
)
type Handler struct {
logger logger.Logger
db *gorm.DB
}
type CreateDomainRequest struct {
Name string `json:"name" binding:"required"`
}
func NewHandler(l logger.Logger, db *gorm.DB) *Handler {
return &Handler{logger: l, db: db}
}
// @Summary List Domains
// @Description Get all whitelisted domains for the current user
// @Tags domains
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /domains [get]
// @Security BearerAuth
func (h *Handler) ListDomains(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var items []model.Domain
if err := h.db.WithContext(c.Request.Context()).
Where("user_id = ?", userID).
Order("created_at DESC").
Find(&items).Error; err != nil {
h.logger.Error("Failed to list domains", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to load domains")
return
}
response.Success(c, gin.H{"domains": items})
}
// @Summary Create Domain
// @Description Add a domain to the current user's whitelist
// @Tags domains
// @Accept json
// @Produce json
// @Param request body CreateDomainRequest true "Domain payload"
// @Success 201 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /domains [post]
// @Security BearerAuth
func (h *Handler) CreateDomain(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var req CreateDomainRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
name := normalizeDomain(req.Name)
if name == "" || !strings.Contains(name, ".") || strings.ContainsAny(name, "/ ") {
response.Error(c, http.StatusBadRequest, "Invalid domain")
return
}
var count int64
if err := h.db.WithContext(c.Request.Context()).
Model(&model.Domain{}).
Where("user_id = ? AND name = ?", userID, name).
Count(&count).Error; err != nil {
h.logger.Error("Failed to validate domain", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create domain")
return
}
if count > 0 {
response.Error(c, http.StatusBadRequest, "Domain already exists")
return
}
item := &model.Domain{
ID: uuid.New().String(),
UserID: userID,
Name: name,
}
if err := h.db.WithContext(c.Request.Context()).Create(item).Error; err != nil {
h.logger.Error("Failed to create domain", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create domain")
return
}
response.Created(c, gin.H{"domain": item})
}
// @Summary Delete Domain
// @Description Remove a domain from the current user's whitelist
// @Tags domains
// @Produce json
// @Param id path string true "Domain ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /domains/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteDomain(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Domain not found")
return
}
result := h.db.WithContext(c.Request.Context()).
Where("id = ? AND user_id = ?", id, userID).
Delete(&model.Domain{})
if result.Error != nil {
h.logger.Error("Failed to delete domain", "error", result.Error)
response.Error(c, http.StatusInternalServerError, "Failed to delete domain")
return
}
if result.RowsAffected == 0 {
response.Error(c, http.StatusNotFound, "Domain not found")
return
}
response.Success(c, gin.H{"message": "Domain deleted"})
}
func normalizeDomain(value string) string {
normalized := strings.TrimSpace(strings.ToLower(value))
normalized = strings.TrimPrefix(normalized, "https://")
normalized = strings.TrimPrefix(normalized, "http://")
normalized = strings.TrimPrefix(normalized, "www.")
normalized = strings.TrimSuffix(normalized, "/")
return normalized
}

View File

@@ -0,0 +1,246 @@
//go:build ignore
// +build ignore
package notifications
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/logger"
"stream.api/pkg/response"
)
type Handler struct {
logger logger.Logger
db *gorm.DB
}
type NotificationItem struct {
ID string `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Message string `json:"message"`
Read bool `json:"read"`
ActionURL string `json:"actionUrl,omitempty"`
ActionLabel string `json:"actionLabel,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
func NewHandler(l logger.Logger, db *gorm.DB) *Handler {
return &Handler{logger: l, db: db}
}
// @Summary List Notifications
// @Description Get notifications for the current user
// @Tags notifications
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /notifications [get]
// @Security BearerAuth
func (h *Handler) ListNotifications(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var rows []model.Notification
if err := h.db.WithContext(c.Request.Context()).
Where("user_id = ?", userID).
Order("created_at DESC").
Find(&rows).Error; err != nil {
h.logger.Error("Failed to list notifications", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to load notifications")
return
}
items := make([]NotificationItem, 0, len(rows))
for _, row := range rows {
items = append(items, mapNotification(row))
}
response.Success(c, gin.H{"notifications": items})
}
// @Summary Mark Notification Read
// @Description Mark a single notification as read for the current user
// @Tags notifications
// @Produce json
// @Param id path string true "Notification ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /notifications/{id}/read [post]
// @Security BearerAuth
func (h *Handler) MarkRead(c *gin.Context) {
h.updateReadState(c, true, false)
}
// @Summary Mark All Notifications Read
// @Description Mark all notifications as read for the current user
// @Tags notifications
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /notifications/read-all [post]
// @Security BearerAuth
func (h *Handler) MarkAllRead(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if err := h.db.WithContext(c.Request.Context()).
Model(&model.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Update("is_read", true).Error; err != nil {
h.logger.Error("Failed to mark all notifications as read", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update notifications")
return
}
response.Success(c, gin.H{"message": "All notifications marked as read"})
}
// @Summary Delete Notification
// @Description Delete a single notification for the current user
// @Tags notifications
// @Produce json
// @Param id path string true "Notification ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /notifications/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteNotification(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Notification not found")
return
}
result := h.db.WithContext(c.Request.Context()).
Where("id = ? AND user_id = ?", id, userID).
Delete(&model.Notification{})
if result.Error != nil {
h.logger.Error("Failed to delete notification", "error", result.Error)
response.Error(c, http.StatusInternalServerError, "Failed to delete notification")
return
}
if result.RowsAffected == 0 {
response.Error(c, http.StatusNotFound, "Notification not found")
return
}
response.Success(c, gin.H{"message": "Notification deleted"})
}
// @Summary Clear Notifications
// @Description Delete all notifications for the current user
// @Tags notifications
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /notifications [delete]
// @Security BearerAuth
func (h *Handler) ClearNotifications(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if err := h.db.WithContext(c.Request.Context()).Where("user_id = ?", userID).Delete(&model.Notification{}).Error; err != nil {
h.logger.Error("Failed to clear notifications", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to clear notifications")
return
}
response.Success(c, gin.H{"message": "All notifications deleted"})
}
func (h *Handler) updateReadState(c *gin.Context, value bool, silentNotFound bool) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Notification not found")
return
}
result := h.db.WithContext(c.Request.Context()).
Model(&model.Notification{}).
Where("id = ? AND user_id = ?", id, userID).
Update("is_read", value)
if result.Error != nil {
h.logger.Error("Failed to update notification", "error", result.Error)
response.Error(c, http.StatusInternalServerError, "Failed to update notification")
return
}
if result.RowsAffected == 0 && !silentNotFound {
response.Error(c, http.StatusNotFound, "Notification not found")
return
}
response.Success(c, gin.H{"message": "Notification updated"})
}
func mapNotification(item model.Notification) NotificationItem {
createdAt := time.Time{}
if item.CreatedAt != nil {
createdAt = item.CreatedAt.UTC()
}
return NotificationItem{
ID: item.ID,
Type: normalizeType(item.Type),
Title: item.Title,
Message: item.Message,
Read: item.IsRead,
ActionURL: model.StringValue(item.ActionURL),
ActionLabel: model.StringValue(item.ActionLabel),
CreatedAt: createdAt,
}
}
func normalizeType(value string) string {
lower := strings.ToLower(strings.TrimSpace(value))
switch {
case strings.Contains(lower, "video"):
return "video"
case strings.Contains(lower, "payment"), strings.Contains(lower, "billing"):
return "payment"
case strings.Contains(lower, "warning"):
return "warning"
case strings.Contains(lower, "error"):
return "error"
case strings.Contains(lower, "success"):
return "success"
case strings.Contains(lower, "system"):
return "system"
default:
return "info"
}
}

View File

@@ -1,10 +1,21 @@
//go:build ignore
// +build ignore
package payment package payment
import ( import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http" "net/http"
"strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"stream.api/internal/config" "stream.api/internal/config"
"stream.api/internal/database/model" "stream.api/internal/database/model"
"stream.api/internal/database/query" "stream.api/internal/database/query"
@@ -12,20 +23,70 @@ import (
"stream.api/pkg/response" "stream.api/pkg/response"
) )
const (
walletTransactionTypeTopup = "topup"
walletTransactionTypeSubscriptionDebit = "subscription_debit"
paymentMethodWallet = "wallet"
paymentMethodTopup = "topup"
paymentKindSubscription = "subscription"
paymentKindWalletTopup = "wallet_topup"
)
var allowedTermMonths = map[int32]struct{}{
1: {},
3: {},
6: {},
12: {},
}
type Handler struct { type Handler struct {
logger logger.Logger logger logger.Logger
cfg *config.Config cfg *config.Config
db *gorm.DB
} }
func NewHandler(l logger.Logger, cfg *config.Config) PaymentHandler { type paymentRow struct {
ID string `gorm:"column:id"`
Amount float64 `gorm:"column:amount"`
Currency *string `gorm:"column:currency"`
Status *string `gorm:"column:status"`
PlanID *string `gorm:"column:plan_id"`
PlanName *string `gorm:"column:plan_name"`
TermMonths *int32 `gorm:"column:term_months"`
PaymentMethod *string `gorm:"column:payment_method"`
ExpiresAt *time.Time `gorm:"column:expires_at"`
CreatedAt *time.Time `gorm:"column:created_at"`
}
type paymentInvoiceDetails struct {
PlanName string
TermMonths *int32
PaymentMethod string
ExpiresAt *time.Time
WalletAmount float64
TopupAmount float64
}
type paymentError struct {
Code int
Message string
Data interface{}
}
func (e *paymentError) Error() string {
return e.Message
}
func NewHandler(l logger.Logger, cfg *config.Config, db *gorm.DB) PaymentHandler {
return &Handler{ return &Handler{
logger: l, logger: l,
cfg: cfg, cfg: cfg,
db: db,
} }
} }
// @Summary Create Payment // @Summary Create Payment
// @Description Create a new payment // @Description Create a new payment for buying or renewing a plan
// @Tags payment // @Tags payment
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -33,6 +94,7 @@ func NewHandler(l logger.Logger, cfg *config.Config) PaymentHandler {
// @Success 201 {object} response.Response // @Success 201 {object} response.Response
// @Failure 400 {object} response.Response // @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response // @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /payments [post] // @Router /payments [post]
// @Security BearerAuth // @Security BearerAuth
@@ -49,27 +111,696 @@ func (h *Handler) CreatePayment(c *gin.Context) {
return return
} }
// In a real scenario, we would contact Stripe/PayPal here to create a session planID := strings.TrimSpace(req.PlanID)
// For now, we just create a "PENDING" payment record. if planID == "" {
response.Error(c, http.StatusBadRequest, "Plan ID is required")
status := "PENDING" return
provider := "STRIPE" }
if !isAllowedTermMonths(req.TermMonths) {
payment := &model.Payment{ response.Error(c, http.StatusBadRequest, "Term months must be one of 1, 3, 6, or 12")
ID: uuid.New().String(), return
UserID: userID,
PlanID: &req.PlanID,
Amount: req.Amount,
Status: &status,
Provider: &provider, // Defaulting to Stripe for this example
} }
p := query.Payment paymentMethod := normalizePaymentMethod(req.PaymentMethod)
if err := p.WithContext(c.Request.Context()).Create(payment); err != nil { if paymentMethod == "" {
response.Error(c, http.StatusBadRequest, "Payment method must be wallet or topup")
return
}
ctx := c.Request.Context()
var planRecord model.Plan
if err := h.db.WithContext(ctx).Where("id = ?", planID).First(&planRecord).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Plan not found")
return
}
h.logger.Error("Failed to load plan", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create payment")
return
}
if planRecord.IsActive == nil || !*planRecord.IsActive {
response.Error(c, http.StatusBadRequest, "Plan is not active")
return
}
totalAmount := planRecord.Price * float64(req.TermMonths)
if totalAmount < 0 {
response.Error(c, http.StatusBadRequest, "Amount must be greater than or equal to 0")
return
}
status := "SUCCESS"
provider := "INTERNAL"
currency := normalizeCurrency(nil)
transactionID := buildTransactionID("sub")
now := time.Now().UTC()
payment := &model.Payment{
ID: uuid.New().String(),
UserID: userID,
PlanID: &planRecord.ID,
Amount: totalAmount,
Currency: &currency,
Status: &status,
Provider: &provider,
TransactionID: &transactionID,
}
invoiceID := buildInvoiceID(payment.ID)
var subscription *model.PlanSubscription
var walletBalance float64
err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if _, err := lockUserForUpdate(ctx, tx, userID); err != nil {
return err
}
currentSubscription, err := model.GetLatestPlanSubscription(ctx, tx, userID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
baseExpiry := now
if currentSubscription != nil && currentSubscription.ExpiresAt.After(baseExpiry) {
baseExpiry = currentSubscription.ExpiresAt.UTC()
}
newExpiry := baseExpiry.AddDate(0, int(req.TermMonths), 0)
currentWalletBalance, err := model.GetWalletBalance(ctx, tx, userID)
if err != nil {
return err
}
shortfall := maxFloat(totalAmount-currentWalletBalance, 0)
if paymentMethod == paymentMethodWallet && shortfall > 0 {
return &paymentError{
Code: http.StatusBadRequest,
Message: "Insufficient wallet balance",
Data: gin.H{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
},
}
}
topupAmount := 0.0
if paymentMethod == paymentMethodTopup {
if req.TopupAmount == nil {
return &paymentError{
Code: http.StatusBadRequest,
Message: "Top-up amount is required when payment method is topup",
Data: gin.H{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
},
}
}
topupAmount = maxFloat(*req.TopupAmount, 0)
if topupAmount <= 0 {
return &paymentError{
Code: http.StatusBadRequest,
Message: "Top-up amount must be greater than 0",
Data: gin.H{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
},
}
}
if topupAmount < shortfall {
return &paymentError{
Code: http.StatusBadRequest,
Message: "Top-up amount must be greater than or equal to the required shortfall",
Data: gin.H{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
"topup_amount": topupAmount,
},
}
}
}
if err := tx.Create(payment).Error; err != nil {
return err
}
walletUsedAmount := totalAmount
if paymentMethod == paymentMethodTopup {
topupTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: userID,
Type: walletTransactionTypeTopup,
Amount: topupAmount,
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Wallet top-up for %s (%d months)", planRecord.Name, req.TermMonths)),
PaymentID: &payment.ID,
PlanID: &planRecord.ID,
TermMonths: &req.TermMonths,
}
if err := tx.Create(topupTransaction).Error; err != nil {
return err
}
}
debitTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: userID,
Type: walletTransactionTypeSubscriptionDebit,
Amount: -totalAmount,
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Subscription payment for %s (%d months)", planRecord.Name, req.TermMonths)),
PaymentID: &payment.ID,
PlanID: &planRecord.ID,
TermMonths: &req.TermMonths,
}
if err := tx.Create(debitTransaction).Error; err != nil {
return err
}
subscription = &model.PlanSubscription{
ID: uuid.New().String(),
UserID: userID,
PaymentID: payment.ID,
PlanID: planRecord.ID,
TermMonths: req.TermMonths,
PaymentMethod: paymentMethod,
WalletAmount: walletUsedAmount,
TopupAmount: topupAmount,
StartedAt: now,
ExpiresAt: newExpiry,
}
if err := tx.Create(subscription).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).
Where("id = ?", userID).
Update("plan_id", planRecord.ID).Error; err != nil {
return err
}
notification := buildSubscriptionNotification(userID, payment.ID, invoiceID, &planRecord, subscription)
if err := tx.Create(notification).Error; err != nil {
return err
}
walletBalance, err = model.GetWalletBalance(ctx, tx, userID)
if err != nil {
return err
}
return nil
})
if err != nil {
var paymentErr *paymentError
if errors.As(err, &paymentErr) {
c.AbortWithStatusJSON(paymentErr.Code, response.Response{
Code: paymentErr.Code,
Message: paymentErr.Message,
Data: paymentErr.Data,
})
return
}
h.logger.Error("Failed to create payment", "error", err) h.logger.Error("Failed to create payment", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to create payment") response.Error(c, http.StatusInternalServerError, "Failed to create payment")
return return
} }
response.Created(c, gin.H{"payment": payment, "message": "Payment initiated"}) response.Created(c, gin.H{
"payment": payment,
"subscription": subscription,
"wallet_balance": walletBalance,
"invoice_id": invoiceID,
"message": "Payment completed successfully",
})
}
// @Summary List Payment History
// @Description Get payment history for the current user
// @Tags payment
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /payments/history [get]
// @Security BearerAuth
func (h *Handler) ListPaymentHistory(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var rows []paymentRow
if err := h.db.WithContext(c.Request.Context()).
Table("payment AS p").
Select("p.id, p.amount, p.currency, p.status, p.plan_id, pl.name AS plan_name, ps.term_months, ps.payment_method, ps.expires_at, p.created_at").
Joins("LEFT JOIN plan AS pl ON pl.id = p.plan_id").
Joins("LEFT JOIN plan_subscriptions AS ps ON ps.payment_id = p.id").
Where("p.user_id = ?", userID).
Order("p.created_at DESC").
Scan(&rows).Error; err != nil {
h.logger.Error("Failed to fetch payment history", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to fetch payment history")
return
}
items := make([]PaymentHistoryItem, 0, len(rows))
for _, row := range rows {
items = append(items, PaymentHistoryItem{
ID: row.ID,
Amount: row.Amount,
Currency: normalizeCurrency(row.Currency),
Status: normalizePaymentStatus(row.Status),
PlanID: row.PlanID,
PlanName: row.PlanName,
InvoiceID: buildInvoiceID(row.ID),
Kind: paymentKindSubscription,
TermMonths: row.TermMonths,
PaymentMethod: normalizeOptionalPaymentMethod(row.PaymentMethod),
ExpiresAt: row.ExpiresAt,
CreatedAt: row.CreatedAt,
})
}
var topups []model.WalletTransaction
if err := h.db.WithContext(c.Request.Context()).
Where("user_id = ? AND type = ? AND payment_id IS NULL", userID, walletTransactionTypeTopup).
Order("created_at DESC").
Find(&topups).Error; err != nil {
h.logger.Error("Failed to fetch wallet topups", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to fetch payment history")
return
}
for _, topup := range topups {
createdAt := topup.CreatedAt
items = append(items, PaymentHistoryItem{
ID: topup.ID,
Amount: topup.Amount,
Currency: normalizeCurrency(topup.Currency),
Status: "success",
InvoiceID: buildInvoiceID(topup.ID),
Kind: paymentKindWalletTopup,
CreatedAt: createdAt,
})
}
sortPaymentHistory(items)
response.Success(c, gin.H{"payments": items})
}
// @Summary Top Up Wallet
// @Description Add funds to wallet balance for the current user
// @Tags payment
// @Accept json
// @Produce json
// @Param request body TopupWalletRequest true "Topup Info"
// @Success 201 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /wallet/topups [post]
// @Security BearerAuth
func (h *Handler) TopupWallet(c *gin.Context) {
var req TopupWalletRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
amount := req.Amount
if amount < 1 {
response.Error(c, http.StatusBadRequest, "Amount must be at least 1")
return
}
transaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: userID,
Type: walletTransactionTypeTopup,
Amount: amount,
Currency: model.StringPtr("USD"),
Note: model.StringPtr(fmt.Sprintf("Wallet top-up of %.2f USD", amount)),
}
notification := &model.Notification{
ID: uuid.New().String(),
UserID: userID,
Type: "billing.topup",
Title: "Wallet credited",
Message: fmt.Sprintf("Your wallet has been credited with %.2f USD.", amount),
Metadata: model.StringPtr(mustMarshalJSON(gin.H{
"wallet_transaction_id": transaction.ID,
"invoice_id": buildInvoiceID(transaction.ID),
})),
}
if err := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
if _, err := lockUserForUpdate(c.Request.Context(), tx, userID); err != nil {
return err
}
if err := tx.Create(transaction).Error; err != nil {
return err
}
if err := tx.Create(notification).Error; err != nil {
return err
}
return nil
}); err != nil {
h.logger.Error("Failed to top up wallet", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to top up wallet")
return
}
balance, err := model.GetWalletBalance(c.Request.Context(), h.db, userID)
if err != nil {
h.logger.Error("Failed to calculate wallet balance", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to top up wallet")
return
}
response.Created(c, gin.H{
"wallet_transaction": transaction,
"wallet_balance": balance,
"invoice_id": buildInvoiceID(transaction.ID),
})
}
// @Summary Download Invoice
// @Description Download invoice text for a payment or wallet top-up
// @Tags payment
// @Produce plain
// @Param id path string true "Payment ID"
// @Success 200 {string} string
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /payments/{id}/invoice [get]
// @Security BearerAuth
func (h *Handler) DownloadInvoice(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Invoice not found")
return
}
ctx := c.Request.Context()
paymentRecord, err := query.Payment.WithContext(ctx).
Where(query.Payment.ID.Eq(id), query.Payment.UserID.Eq(userID)).
First()
if err == nil {
invoiceText, filename, buildErr := h.buildPaymentInvoice(ctx, paymentRecord)
if buildErr != nil {
h.logger.Error("Failed to build payment invoice", "error", buildErr)
response.Error(c, http.StatusInternalServerError, "Failed to download invoice")
return
}
serveInvoiceText(c, filename, invoiceText)
return
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
h.logger.Error("Failed to load payment invoice", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to download invoice")
return
}
var topup model.WalletTransaction
if err := h.db.WithContext(ctx).
Where("id = ? AND user_id = ? AND type = ? AND payment_id IS NULL", id, userID, walletTransactionTypeTopup).
First(&topup).Error; err == nil {
invoiceText := buildTopupInvoice(&topup)
serveInvoiceText(c, buildInvoiceFilename(topup.ID), invoiceText)
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
h.logger.Error("Failed to load topup invoice", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to download invoice")
return
}
response.Error(c, http.StatusNotFound, "Invoice not found")
}
func normalizePaymentStatus(status *string) string {
value := strings.ToLower(strings.TrimSpace(stringValue(status)))
switch value {
case "success", "succeeded", "paid":
return "success"
case "failed", "error", "canceled", "cancelled":
return "failed"
case "pending", "processing":
return "pending"
default:
if value == "" {
return "success"
}
return value
}
}
func normalizeCurrency(currency *string) string {
value := strings.ToUpper(strings.TrimSpace(stringValue(currency)))
if value == "" {
return "USD"
}
return value
}
func normalizePaymentMethod(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case paymentMethodWallet:
return paymentMethodWallet
case paymentMethodTopup:
return paymentMethodTopup
default:
return ""
}
}
func normalizeOptionalPaymentMethod(value *string) *string {
normalized := normalizePaymentMethod(stringValue(value))
if normalized == "" {
return nil
}
return &normalized
}
func buildInvoiceID(id string) string {
trimmed := strings.ReplaceAll(strings.TrimSpace(id), "-", "")
if len(trimmed) > 12 {
trimmed = trimmed[:12]
}
return "INV-" + strings.ToUpper(trimmed)
}
func buildTransactionID(prefix string) string {
return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano())
}
func buildInvoiceFilename(id string) string {
return fmt.Sprintf("invoice-%s.txt", id)
}
func stringValue(value *string) string {
if value == nil {
return ""
}
return *value
}
func sortPaymentHistory(items []PaymentHistoryItem) {
for i := 0; i < len(items); i++ {
for j := i + 1; j < len(items); j++ {
left := time.Time{}
right := time.Time{}
if items[i].CreatedAt != nil {
left = *items[i].CreatedAt
}
if items[j].CreatedAt != nil {
right = *items[j].CreatedAt
}
if right.After(left) {
items[i], items[j] = items[j], items[i]
}
}
}
}
func serveInvoiceText(c *gin.Context, filename string, content string) {
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
c.String(http.StatusOK, content)
}
func (h *Handler) buildPaymentInvoice(ctx context.Context, paymentRecord *model.Payment) (string, string, error) {
details, err := h.loadPaymentInvoiceDetails(ctx, paymentRecord)
if err != nil {
return "", "", err
}
createdAt := formatOptionalTimestamp(paymentRecord.CreatedAt)
lines := []string{
"Stream API Invoice",
fmt.Sprintf("Invoice ID: %s", buildInvoiceID(paymentRecord.ID)),
fmt.Sprintf("Payment ID: %s", paymentRecord.ID),
fmt.Sprintf("User ID: %s", paymentRecord.UserID),
fmt.Sprintf("Plan: %s", details.PlanName),
fmt.Sprintf("Amount: %.2f %s", paymentRecord.Amount, normalizeCurrency(paymentRecord.Currency)),
fmt.Sprintf("Status: %s", strings.ToUpper(normalizePaymentStatus(paymentRecord.Status))),
fmt.Sprintf("Provider: %s", strings.ToUpper(stringValue(paymentRecord.Provider))),
fmt.Sprintf("Payment Method: %s", strings.ToUpper(details.PaymentMethod)),
fmt.Sprintf("Transaction ID: %s", stringValue(paymentRecord.TransactionID)),
}
if details.TermMonths != nil {
lines = append(lines, fmt.Sprintf("Term: %d month(s)", *details.TermMonths))
}
if details.ExpiresAt != nil {
lines = append(lines, fmt.Sprintf("Valid Until: %s", details.ExpiresAt.UTC().Format(time.RFC3339)))
}
if details.WalletAmount > 0 {
lines = append(lines, fmt.Sprintf("Wallet Applied: %.2f %s", details.WalletAmount, normalizeCurrency(paymentRecord.Currency)))
}
if details.TopupAmount > 0 {
lines = append(lines, fmt.Sprintf("Top-up Added: %.2f %s", details.TopupAmount, normalizeCurrency(paymentRecord.Currency)))
}
lines = append(lines, fmt.Sprintf("Created At: %s", createdAt))
return strings.Join(lines, "\n"), buildInvoiceFilename(paymentRecord.ID), nil
}
func buildTopupInvoice(transaction *model.WalletTransaction) string {
createdAt := formatOptionalTimestamp(transaction.CreatedAt)
return strings.Join([]string{
"Stream API Wallet Top-up Invoice",
fmt.Sprintf("Invoice ID: %s", buildInvoiceID(transaction.ID)),
fmt.Sprintf("Wallet Transaction ID: %s", transaction.ID),
fmt.Sprintf("User ID: %s", transaction.UserID),
fmt.Sprintf("Amount: %.2f %s", transaction.Amount, normalizeCurrency(transaction.Currency)),
"Status: SUCCESS",
fmt.Sprintf("Type: %s", strings.ToUpper(transaction.Type)),
fmt.Sprintf("Note: %s", model.StringValue(transaction.Note)),
fmt.Sprintf("Created At: %s", createdAt),
}, "\n")
}
func (h *Handler) loadPaymentInvoiceDetails(ctx context.Context, paymentRecord *model.Payment) (*paymentInvoiceDetails, error) {
details := &paymentInvoiceDetails{
PlanName: "Unknown plan",
PaymentMethod: paymentMethodWallet,
}
if paymentRecord.PlanID != nil && strings.TrimSpace(*paymentRecord.PlanID) != "" {
var planRecord model.Plan
if err := h.db.WithContext(ctx).Where("id = ?", *paymentRecord.PlanID).First(&planRecord).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
} else {
details.PlanName = planRecord.Name
}
}
var subscription model.PlanSubscription
if err := h.db.WithContext(ctx).
Where("payment_id = ?", paymentRecord.ID).
Order("created_at DESC").
First(&subscription).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
return details, nil
}
details.TermMonths = &subscription.TermMonths
details.PaymentMethod = normalizePaymentMethod(subscription.PaymentMethod)
if details.PaymentMethod == "" {
details.PaymentMethod = paymentMethodWallet
}
details.ExpiresAt = &subscription.ExpiresAt
details.WalletAmount = subscription.WalletAmount
details.TopupAmount = subscription.TopupAmount
return details, nil
}
func buildSubscriptionNotification(userID string, paymentID string, invoiceID string, planRecord *model.Plan, subscription *model.PlanSubscription) *model.Notification {
return &model.Notification{
ID: uuid.New().String(),
UserID: userID,
Type: "billing.subscription",
Title: "Subscription activated",
Message: fmt.Sprintf("Your subscription to %s is active until %s.", planRecord.Name, subscription.ExpiresAt.UTC().Format("2006-01-02")),
Metadata: model.StringPtr(mustMarshalJSON(gin.H{
"payment_id": paymentID,
"invoice_id": invoiceID,
"plan_id": planRecord.ID,
"term_months": subscription.TermMonths,
"payment_method": subscription.PaymentMethod,
"wallet_amount": subscription.WalletAmount,
"topup_amount": subscription.TopupAmount,
"plan_expires_at": subscription.ExpiresAt.UTC().Format(time.RFC3339),
})),
}
}
func isAllowedTermMonths(value int32) bool {
_, ok := allowedTermMonths[value]
return ok
}
func lockUserForUpdate(ctx context.Context, tx *gorm.DB, userID string) (*model.User, error) {
var user model.User
if err := tx.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", userID).
First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func maxFloat(left float64, right float64) float64 {
if left > right {
return left
}
return right
}
func formatOptionalTimestamp(value *time.Time) string {
if value == nil {
return ""
}
return value.UTC().Format(time.RFC3339)
}
func mustMarshalJSON(value interface{}) string {
encoded, err := json.Marshal(value)
if err != nil {
return "{}"
}
return string(encoded)
} }

View File

@@ -0,0 +1,12 @@
package payment
type CreatePaymentRequest struct {
PlanID string `json:"plan_id" binding:"required"`
TermMonths int32 `json:"term_months" binding:"required"`
PaymentMethod string `json:"payment_method" binding:"required"`
TopupAmount *float64 `json:"topup_amount,omitempty"`
}
type TopupWalletRequest struct {
Amount float64 `json:"amount" binding:"required"`
}

View File

@@ -1,3 +1,6 @@
//go:build ignore
// +build ignore
package payment package payment
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
@@ -5,10 +8,19 @@ import "github.com/gin-gonic/gin"
// PaymentHandler defines the interface for payment operations // PaymentHandler defines the interface for payment operations
type PaymentHandler interface { type PaymentHandler interface {
CreatePayment(c *gin.Context) CreatePayment(c *gin.Context)
ListPaymentHistory(c *gin.Context)
TopupWallet(c *gin.Context)
DownloadInvoice(c *gin.Context)
} }
// CreatePaymentRequest defines the payload for creating a payment // CreatePaymentRequest defines the payload for creating a payment
type CreatePaymentRequest struct { type CreatePaymentRequest struct {
PlanID string `json:"plan_id" binding:"required"` PlanID string `json:"plan_id" binding:"required"`
TermMonths int32 `json:"term_months" binding:"required"`
PaymentMethod string `json:"payment_method" binding:"required"`
TopupAmount *float64 `json:"topup_amount,omitempty"`
}
type TopupWalletRequest struct {
Amount float64 `json:"amount" binding:"required"` Amount float64 `json:"amount" binding:"required"`
} }

View File

@@ -0,0 +1,18 @@
package payment
import "time"
type PaymentHistoryItem struct {
ID string `json:"id"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"`
PlanID *string `json:"plan_id,omitempty"`
PlanName *string `json:"plan_name,omitempty"`
InvoiceID string `json:"invoice_id"`
Kind string `json:"kind"`
TermMonths *int32 `json:"term_months,omitempty"`
PaymentMethod *string `json:"payment_method,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
}

View File

@@ -1,11 +1,15 @@
//go:build ignore
// +build ignore
package plan package plan
import ( import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"stream.api/internal/config" "stream.api/internal/config"
"stream.api/internal/database/query" "stream.api/internal/database/model"
"stream.api/pkg/logger" "stream.api/pkg/logger"
"stream.api/pkg/response" "stream.api/pkg/response"
) )
@@ -13,12 +17,14 @@ import (
type Handler struct { type Handler struct {
logger logger.Logger logger logger.Logger
cfg *config.Config cfg *config.Config
db *gorm.DB
} }
func NewHandler(l logger.Logger, cfg *config.Config) PlanHandler { func NewHandler(l logger.Logger, cfg *config.Config, db *gorm.DB) PlanHandler {
return &Handler{ return &Handler{
logger: l, logger: l,
cfg: cfg, cfg: cfg,
db: db,
} }
} }
@@ -26,14 +32,13 @@ func NewHandler(l logger.Logger, cfg *config.Config) PlanHandler {
// @Description Get all active plans // @Description Get all active plans
// @Tags plan // @Tags plan
// @Produce json // @Produce json
// @Success 200 {object} response.Response{data=[]model.Plan} // @Success 200 {object} response.Response
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /plans [get] // @Router /plans [get]
// @Security BearerAuth // @Security BearerAuth
func (h *Handler) ListPlans(c *gin.Context) { func (h *Handler) ListPlans(c *gin.Context) {
p := query.Plan var plans []model.Plan
plans, err := p.WithContext(c.Request.Context()).Where(p.IsActive.Is(true)).Find() if err := h.db.WithContext(c.Request.Context()).Where("is_active = ?", true).Find(&plans).Error; err != nil {
if err != nil {
h.logger.Error("Failed to fetch plans", "error", err) h.logger.Error("Failed to fetch plans", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to fetch plans") response.Error(c, http.StatusInternalServerError, "Failed to fetch plans")
return return

View File

@@ -1,3 +1,6 @@
//go:build ignore
// +build ignore
package plan package plan
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"

View File

@@ -0,0 +1,112 @@
//go:build ignore
// +build ignore
package preferences
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"stream.api/pkg/logger"
"stream.api/pkg/response"
)
type Handler struct {
logger logger.Logger
db *gorm.DB
}
type SettingsPreferencesRequest struct {
EmailNotifications *bool `json:"email_notifications"`
PushNotifications *bool `json:"push_notifications"`
MarketingNotifications *bool `json:"marketing_notifications"`
TelegramNotifications *bool `json:"telegram_notifications"`
Autoplay *bool `json:"autoplay"`
Loop *bool `json:"loop"`
Muted *bool `json:"muted"`
ShowControls *bool `json:"show_controls"`
Pip *bool `json:"pip"`
Airplay *bool `json:"airplay"`
Chromecast *bool `json:"chromecast"`
Language *string `json:"language"`
Locale *string `json:"locale"`
}
func NewHandler(l logger.Logger, db *gorm.DB) *Handler {
return &Handler{logger: l, db: db}
}
// @Summary Get Preferences
// @Description Get notification, player, and locale preferences for the current user
// @Tags settings
// @Produce json
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /settings/preferences [get]
// @Security BearerAuth
func (h *Handler) GetPreferences(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
pref, err := LoadUserPreferences(c.Request.Context(), h.db, userID)
if err != nil {
h.logger.Error("Failed to load preferences", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to load preferences")
return
}
response.Success(c, gin.H{"preferences": pref})
}
// @Summary Update Preferences
// @Description Update notification, player, and locale preferences for the current user
// @Tags settings
// @Accept json
// @Produce json
// @Param request body SettingsPreferencesRequest true "Preferences payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /settings/preferences [put]
// @Security BearerAuth
func (h *Handler) UpdatePreferences(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var req SettingsPreferencesRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
pref, err := UpdateUserPreferences(c.Request.Context(), h.db, h.logger, userID, UpdateInput{
EmailNotifications: req.EmailNotifications,
PushNotifications: req.PushNotifications,
MarketingNotifications: req.MarketingNotifications,
TelegramNotifications: req.TelegramNotifications,
Autoplay: req.Autoplay,
Loop: req.Loop,
Muted: req.Muted,
ShowControls: req.ShowControls,
Pip: req.Pip,
Airplay: req.Airplay,
Chromecast: req.Chromecast,
Language: req.Language,
Locale: req.Locale,
})
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to save preferences")
return
}
response.Success(c, gin.H{"preferences": pref})
}

View File

@@ -0,0 +1,91 @@
package preferences
import (
"context"
"strings"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/logger"
)
type UpdateInput struct {
EmailNotifications *bool
PushNotifications *bool
MarketingNotifications *bool
TelegramNotifications *bool
Autoplay *bool
Loop *bool
Muted *bool
ShowControls *bool
Pip *bool
Airplay *bool
Chromecast *bool
Language *string
Locale *string
}
func LoadUserPreferences(ctx context.Context, db *gorm.DB, userID string) (*model.UserPreference, error) {
return model.FindOrCreateUserPreference(ctx, db, userID)
}
func UpdateUserPreferences(ctx context.Context, db *gorm.DB, l logger.Logger, userID string, req UpdateInput) (*model.UserPreference, error) {
pref, err := model.FindOrCreateUserPreference(ctx, db, userID)
if err != nil {
l.Error("Failed to load preferences", "error", err)
return nil, err
}
if req.EmailNotifications != nil {
pref.EmailNotifications = model.BoolPtr(*req.EmailNotifications)
}
if req.PushNotifications != nil {
pref.PushNotifications = model.BoolPtr(*req.PushNotifications)
}
if req.MarketingNotifications != nil {
pref.MarketingNotifications = *req.MarketingNotifications
}
if req.TelegramNotifications != nil {
pref.TelegramNotifications = *req.TelegramNotifications
}
if req.Autoplay != nil {
pref.Autoplay = *req.Autoplay
}
if req.Loop != nil {
pref.Loop = *req.Loop
}
if req.Muted != nil {
pref.Muted = *req.Muted
}
if req.ShowControls != nil {
pref.ShowControls = model.BoolPtr(*req.ShowControls)
}
if req.Pip != nil {
pref.Pip = model.BoolPtr(*req.Pip)
}
if req.Airplay != nil {
pref.Airplay = model.BoolPtr(*req.Airplay)
}
if req.Chromecast != nil {
pref.Chromecast = model.BoolPtr(*req.Chromecast)
}
if req.Language != nil {
pref.Language = model.StringPtr(strings.TrimSpace(*req.Language))
}
if req.Locale != nil {
pref.Locale = model.StringPtr(strings.TrimSpace(*req.Locale))
}
if strings.TrimSpace(model.StringValue(pref.Language)) == "" {
pref.Language = model.StringPtr("en")
}
if strings.TrimSpace(model.StringValue(pref.Locale)) == "" {
pref.Locale = model.StringPtr(model.StringValue(pref.Language))
}
if err := db.WithContext(ctx).Save(pref).Error; err != nil {
l.Error("Failed to save preferences", "error", err)
return nil, err
}
return pref, nil
}

View File

@@ -0,0 +1,63 @@
//go:build ignore
// +build ignore
package usage
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/logger"
"stream.api/pkg/response"
)
type Handler struct {
logger logger.Logger
db *gorm.DB
}
func NewHandler(l logger.Logger, db *gorm.DB) UsageHandler {
return &Handler{
logger: l,
db: db,
}
}
// @Summary Get Usage
// @Description Get the authenticated user's total video count and total storage usage
// @Tags usage
// @Produce json
// @Success 200 {object} response.Response{data=UsagePayload}
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /usage [get]
// @Security BearerAuth
func (h *Handler) GetUsage(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
user, ok := c.Get("user")
if !ok {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
currentUser, ok := user.(*model.User)
if !ok || currentUser == nil || currentUser.ID != userID {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
payload, err := LoadUsage(c.Request.Context(), h.db, h.logger, currentUser)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to load usage")
return
}
response.Success(c, payload)
}

View File

@@ -0,0 +1,11 @@
//go:build ignore
// +build ignore
package usage
import "github.com/gin-gonic/gin"
// UsageHandler defines the interface for usage operations
type UsageHandler interface {
GetUsage(c *gin.Context)
}

View File

@@ -0,0 +1,23 @@
package usage
import (
"context"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/logger"
)
func LoadUsage(ctx context.Context, db *gorm.DB, l logger.Logger, user *model.User) (*UsagePayload, error) {
var totalVideos int64
if err := db.WithContext(ctx).Model(&model.Video{}).Where("user_id = ?", user.ID).Count(&totalVideos).Error; err != nil {
l.Error("Failed to count user videos", "error", err, "user_id", user.ID)
return nil, err
}
return &UsagePayload{
UserID: user.ID,
TotalVideos: totalVideos,
TotalStorage: user.StorageUsed,
}, nil
}

View File

@@ -0,0 +1,7 @@
package usage
type UsagePayload struct {
UserID string `json:"user_id"`
TotalVideos int64 `json:"total_videos"`
TotalStorage int64 `json:"total_storage"`
}

View File

@@ -1,16 +1,22 @@
//go:build ignore
// +build ignore
package video package video
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
"stream.api/internal/config" "stream.api/internal/config"
"stream.api/internal/database/model" "stream.api/internal/database/model"
"stream.api/internal/database/query"
"stream.api/pkg/logger" "stream.api/pkg/logger"
"stream.api/pkg/response" "stream.api/pkg/response"
"stream.api/pkg/storage" "stream.api/pkg/storage"
@@ -19,13 +25,22 @@ import (
type Handler struct { type Handler struct {
logger logger.Logger logger logger.Logger
cfg *config.Config cfg *config.Config
db *gorm.DB
storage storage.Provider storage storage.Provider
} }
func NewHandler(l logger.Logger, cfg *config.Config, s storage.Provider) VideoHandler { type videoError struct {
Code int
Message string
}
func (e *videoError) Error() string { return e.Message }
func NewHandler(l logger.Logger, cfg *config.Config, db *gorm.DB, s storage.Provider) VideoHandler {
return &Handler{ return &Handler{
logger: l, logger: l,
cfg: cfg, cfg: cfg,
db: db,
storage: s, storage: s,
} }
} }
@@ -62,7 +77,7 @@ func (h *Handler) GetUploadURL(c *gin.Context) {
response.Success(c, gin.H{ response.Success(c, gin.H{
"upload_url": url, "upload_url": url,
"key": key, "key": key,
"file_id": fileID, // Temporary ID, actual video record ID might differ or be same "file_id": fileID,
}) })
} }
@@ -85,38 +100,83 @@ func (h *Handler) CreateVideo(c *gin.Context) {
} }
userID := c.GetString("userID") userID := c.GetString("userID")
if userID == "" {
status := "PUBLIC" response.Error(c, http.StatusUnauthorized, "Unauthorized")
storageType := "S3" return
video := &model.Video{
ID: uuid.New().String(),
UserID: userID,
Name: req.Title,
Title: req.Title,
Description: &req.Description,
URL: req.URL,
Size: req.Size,
Duration: req.Duration,
Format: req.Format,
Status: &status,
StorageType: &storageType,
} }
q := query.Q title := strings.TrimSpace(req.Title)
err := q.Transaction(func(tx *query.Query) error { if title == "" {
if err := tx.Video.WithContext(c.Request.Context()).Create(video); err != nil { response.Error(c, http.StatusBadRequest, "Title is required")
return
}
videoURL := strings.TrimSpace(req.URL)
if videoURL == "" {
response.Error(c, http.StatusBadRequest, "URL is required")
return
}
status := "ready"
processingStatus := "READY"
storageType := detectStorageType(videoURL)
description := strings.TrimSpace(req.Description)
format := strings.TrimSpace(req.Format)
video := &model.Video{
ID: uuid.New().String(),
UserID: userID,
Name: title,
Title: title,
Description: stringPointer(description),
URL: videoURL,
Size: req.Size,
Duration: req.Duration,
Format: format,
Status: &status,
ProcessingStatus: &processingStatus,
StorageType: &storageType,
}
err := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
var defaultTemplate model.AdTemplate
hasDefaultTemplate := false
if err := tx.Where("user_id = ? AND is_default = ? AND is_active = ?", userID, true, true).
Order("updated_at DESC").
First(&defaultTemplate).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
} else {
hasDefaultTemplate = true
}
if err := tx.Create(video).Error; err != nil {
return err return err
} }
// Atomic update: StorageUsed = StorageUsed + video.Size if err := tx.Model(&model.User{}).
// We use UpdateSimple with Add to ensure atomicity at database level: UPDATE users SET storage_used = storage_used + ? Where("id = ?", userID).
if _, err := tx.User.WithContext(c.Request.Context()). UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
Where(tx.User.ID.Eq(userID)).
UpdateSimple(tx.User.StorageUsed.Add(video.Size)); err != nil {
return err return err
} }
if hasDefaultTemplate {
videoAdConfig := &model.VideoAdConfig{
VideoID: video.ID,
UserID: userID,
AdTemplateID: defaultTemplate.ID,
VastTagURL: defaultTemplate.VastTagURL,
AdFormat: defaultTemplate.AdFormat,
Duration: defaultTemplate.Duration,
}
if err := tx.Create(videoAdConfig).Error; err != nil {
return err
}
}
return nil return nil
}) })
@@ -140,17 +200,46 @@ func (h *Handler) CreateVideo(c *gin.Context) {
// @Router /videos [get] // @Router /videos [get]
// @Security BearerAuth // @Security BearerAuth
func (h *Handler) ListVideos(c *gin.Context) { func (h *Handler) ListVideos(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limit <= 0 {
limit = 10
}
if limit > 100 {
limit = 100
}
offset := (page - 1) * limit offset := (page - 1) * limit
v := query.Video search := strings.TrimSpace(c.Query("search"))
videos, count, err := v.WithContext(c.Request.Context()). status := strings.TrimSpace(c.Query("status"))
Where(v.Status.Eq("PUBLIC")).
Order(v.CreatedAt.Desc()).
FindByPage(offset, limit)
if err != nil { db := h.db.WithContext(c.Request.Context()).Model(&model.Video{}).Where("user_id = ?", userID)
if search != "" {
like := "%" + search + "%"
db = db.Where("title ILIKE ? OR description ILIKE ?", like, like)
}
if status != "" && !strings.EqualFold(status, "all") {
db = db.Where("status = ?", normalizeVideoStatus(status))
}
var total int64
if err := db.Count(&total).Error; err != nil {
h.logger.Error("Failed to count videos", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to fetch videos")
return
}
var videos []*model.Video
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&videos).Error; err != nil {
h.logger.Error("Failed to fetch videos", "error", err) h.logger.Error("Failed to fetch videos", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to fetch videos") response.Error(c, http.StatusInternalServerError, "Failed to fetch videos")
return return
@@ -158,7 +247,7 @@ func (h *Handler) ListVideos(c *gin.Context) {
response.Success(c, gin.H{ response.Success(c, gin.H{
"videos": videos, "videos": videos,
"total": count, "total": total,
"page": page, "page": page,
"limit": limit, "limit": limit,
}) })
@@ -174,19 +263,321 @@ func (h *Handler) ListVideos(c *gin.Context) {
// @Router /videos/{id} [get] // @Router /videos/{id} [get]
// @Security BearerAuth // @Security BearerAuth
func (h *Handler) GetVideo(c *gin.Context) { func (h *Handler) GetVideo(c *gin.Context) {
id := c.Param("id") userID := c.GetString("userID")
v := query.Video if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
// Atomically increment views: UPDATE videos SET views = views + 1 WHERE id = ?
// We intentionally ignore errors here (like record not found) because the subsequent fetch will handle 404s,
// and we don't want to fail the read if writing the view count fails for some transient reason.
v.WithContext(c.Request.Context()).Where(v.ID.Eq(id)).UpdateSimple(v.Views.Add(1))
video, err := v.WithContext(c.Request.Context()).Where(v.ID.Eq(id)).First()
if err != nil {
response.Error(c, http.StatusNotFound, "Video not found")
return return
} }
response.Success(c, gin.H{"video": video}) id := c.Param("id")
h.db.WithContext(c.Request.Context()).Model(&model.Video{}).
Where("id = ? AND user_id = ?", id, userID).
UpdateColumn("views", gorm.Expr("views + ?", 1))
var video model.Video
if err := h.db.WithContext(c.Request.Context()).Where("id = ? AND user_id = ?", id, userID).First(&video).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
h.logger.Error("Failed to fetch video", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to fetch video")
return
}
result := gin.H{"video": &video}
var adConfig model.VideoAdConfig
if err := h.db.WithContext(c.Request.Context()).
Where("video_id = ? AND user_id = ?", id, userID).
First(&adConfig).Error; err == nil {
adPayload := VideoAdConfigPayload{
AdTemplateID: adConfig.AdTemplateID,
VASTTagURL: adConfig.VastTagURL,
AdFormat: model.StringValue(adConfig.AdFormat),
Duration: int64PtrToIntPtr(adConfig.Duration),
}
var template model.AdTemplate
if err := h.db.WithContext(c.Request.Context()).
Where("id = ? AND user_id = ?", adConfig.AdTemplateID, userID).
First(&template).Error; err == nil {
adPayload.TemplateName = template.Name
}
result["ad_config"] = adPayload
}
response.Success(c, result)
}
// @Summary Update Video
// @Description Update title and description for a video owned by the current user
// @Tags video
// @Accept json
// @Produce json
// @Param id path string true "Video ID"
// @Param request body UpdateVideoRequest true "Video payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /videos/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdateVideo(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
id := c.Param("id")
var req UpdateVideoRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
title := strings.TrimSpace(req.Title)
if title == "" {
response.Error(c, http.StatusBadRequest, "Title is required")
return
}
description := strings.TrimSpace(req.Description)
ctx := c.Request.Context()
err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Video{}).
Where("id = ? AND user_id = ?", id, userID).
Updates(map[string]interface{}{
"name": title,
"title": title,
"description": stringPointer(description),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
if req.AdTemplateID != nil {
templateID := strings.TrimSpace(*req.AdTemplateID)
if templateID == "" {
tx.Where("video_id = ? AND user_id = ?", id, userID).Delete(&model.VideoAdConfig{})
} else {
var template model.AdTemplate
if err := tx.Where("id = ? AND user_id = ?", templateID, userID).
First(&template).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &videoError{Code: http.StatusBadRequest, Message: "Ad template not found"}
}
return err
}
var existing model.VideoAdConfig
if err := tx.Where("video_id = ? AND user_id = ?", id, userID).
First(&existing).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
newConfig := &model.VideoAdConfig{
VideoID: id,
UserID: userID,
AdTemplateID: template.ID,
VastTagURL: template.VastTagURL,
AdFormat: template.AdFormat,
Duration: template.Duration,
}
return tx.Create(newConfig).Error
}
return err
}
existing.AdTemplateID = template.ID
existing.VastTagURL = template.VastTagURL
existing.AdFormat = template.AdFormat
existing.Duration = template.Duration
return tx.Save(&existing).Error
}
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
var ve *videoError
if errors.As(err, &ve) {
response.Error(c, ve.Code, ve.Message)
return
}
h.logger.Error("Failed to update video", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update video")
return
}
var video model.Video
if err := h.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&video).Error; err != nil {
h.logger.Error("Failed to reload video", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to update video")
return
}
resp := gin.H{"video": &video}
var adConfig model.VideoAdConfig
if err := h.db.WithContext(ctx).
Where("video_id = ? AND user_id = ?", id, userID).
First(&adConfig).Error; err == nil {
adPayload := VideoAdConfigPayload{
AdTemplateID: adConfig.AdTemplateID,
VASTTagURL: adConfig.VastTagURL,
AdFormat: model.StringValue(adConfig.AdFormat),
Duration: int64PtrToIntPtr(adConfig.Duration),
}
var template model.AdTemplate
if err := h.db.WithContext(ctx).
Where("id = ? AND user_id = ?", adConfig.AdTemplateID, userID).
First(&template).Error; err == nil {
adPayload.TemplateName = template.Name
}
resp["ad_config"] = adPayload
}
response.Success(c, resp)
}
// @Summary Delete Video
// @Description Delete a video owned by the current user
// @Tags video
// @Produce json
// @Param id path string true "Video ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /videos/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteVideo(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
id := c.Param("id")
var video model.Video
if err := h.db.WithContext(c.Request.Context()).Where("id = ? AND user_id = ?", id, userID).First(&video).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Video not found")
return
}
h.logger.Error("Failed to load video for deletion", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to delete video")
return
}
if h.storage != nil && shouldDeleteStoredObject(video.URL) {
if err := h.storage.Delete(video.URL); err != nil {
parsedKey := extractObjectKey(video.URL)
if parsedKey != "" && parsedKey != video.URL {
if deleteErr := h.storage.Delete(parsedKey); deleteErr != nil {
h.logger.Error("Failed to delete video object", "error", deleteErr, "video_id", video.ID)
response.Error(c, http.StatusInternalServerError, "Failed to delete video")
return
}
} else {
h.logger.Error("Failed to delete video object", "error", err, "video_id", video.ID)
response.Error(c, http.StatusInternalServerError, "Failed to delete video")
return
}
}
}
if err := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("video_id = ? AND user_id = ?", video.ID, userID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("id = ? AND user_id = ?", video.ID, userID).Delete(&model.Video{}).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).
Where("id = ?", userID).
UpdateColumn("storage_used", gorm.Expr("storage_used - ?", video.Size)).Error; err != nil {
return err
}
return nil
}); err != nil {
h.logger.Error("Failed to delete video record", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to delete video")
return
}
response.Success(c, gin.H{"message": "Video deleted successfully"})
}
func normalizeVideoStatus(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "processing", "pending":
return "processing"
case "failed", "error":
return "failed"
default:
return "ready"
}
}
func detectStorageType(rawURL string) string {
if shouldDeleteStoredObject(rawURL) {
return "S3"
}
return "WORKER"
}
func shouldDeleteStoredObject(rawURL string) bool {
trimmed := strings.TrimSpace(rawURL)
if trimmed == "" {
return false
}
parsed, err := url.Parse(trimmed)
if err != nil {
return !strings.HasPrefix(trimmed, "/")
}
return parsed.Scheme == "" && parsed.Host == "" && !strings.HasPrefix(trimmed, "/")
}
func extractObjectKey(rawURL string) string {
trimmed := strings.TrimSpace(rawURL)
if trimmed == "" {
return ""
}
parsed, err := url.Parse(trimmed)
if err != nil {
return trimmed
}
if parsed.Scheme == "" && parsed.Host == "" {
return strings.TrimPrefix(parsed.Path, "/")
}
return strings.TrimPrefix(parsed.Path, "/")
}
func stringPointer(value string) *string {
if value == "" {
return nil
}
return &value
}
func int64PtrToIntPtr(value *int64) *int {
if value == nil {
return nil
}
converted := int(*value)
return &converted
} }

View File

@@ -1,3 +1,6 @@
//go:build ignore
// +build ignore
package video package video
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
@@ -8,6 +11,8 @@ type VideoHandler interface {
CreateVideo(c *gin.Context) CreateVideo(c *gin.Context)
ListVideos(c *gin.Context) ListVideos(c *gin.Context)
GetVideo(c *gin.Context) GetVideo(c *gin.Context)
UpdateVideo(c *gin.Context)
DeleteVideo(c *gin.Context)
} }
// UploadURLRequest defines the payload for requesting an upload URL // UploadURLRequest defines the payload for requesting an upload URL
@@ -26,3 +31,17 @@ type CreateVideoRequest struct {
Duration int32 `json:"duration"` // Maybe client knows, or we process later Duration int32 `json:"duration"` // Maybe client knows, or we process later
Format string `json:"format"` Format string `json:"format"`
} }
type UpdateVideoRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
AdTemplateID *string `json:"ad_template_id,omitempty"`
}
type VideoAdConfigPayload struct {
AdTemplateID string `json:"ad_template_id"`
TemplateName string `json:"template_name,omitempty"`
VASTTagURL string `json:"vast_tag_url,omitempty"`
AdFormat string `json:"ad_format,omitempty"`
Duration *int `json:"duration,omitempty"`
}

View File

@@ -1,29 +1,38 @@
//go:build ignore
// +build ignore
package app package app
import ( import (
"context"
"net/http" "net/http"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"stream.api/internal/api/admin"
"stream.api/internal/api/adtemplates"
"stream.api/internal/api/auth" "stream.api/internal/api/auth"
"stream.api/internal/api/domains"
"stream.api/internal/api/notifications"
"stream.api/internal/api/payment" "stream.api/internal/api/payment"
"stream.api/internal/api/plan" "stream.api/internal/api/plan"
"stream.api/internal/api/preferences"
"stream.api/internal/api/usage"
"stream.api/internal/api/video" "stream.api/internal/api/video"
"stream.api/internal/config" "stream.api/internal/config"
"stream.api/internal/middleware" "stream.api/internal/middleware"
videoruntime "stream.api/internal/video/runtime"
"stream.api/pkg/cache" "stream.api/pkg/cache"
"stream.api/pkg/logger" "stream.api/pkg/logger"
"stream.api/pkg/response"
"stream.api/pkg/storage" "stream.api/pkg/storage"
"stream.api/pkg/token" "stream.api/pkg/token"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
_ "stream.api/docs"
) )
func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provider, l logger.Logger) *gin.Engine { func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provider, l logger.Logger) (*gin.Engine, *videoruntime.Module, error) {
if cfg.Server.Mode == "release" { if cfg.Server.Mode == "release" {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
@@ -36,7 +45,7 @@ func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provide
r.Use(middleware.ErrorHandler()) // Handle c.Errors r.Use(middleware.ErrorHandler()) // Handle c.Errors
// CORS Middleware // CORS Middleware
r.Use(cors.New(cors.Config{ r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:5173", "http://localhost:8080"}, AllowOrigins: cfg.CORS.AllowOrigins,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Authorization", "Content-Type"}, AllowHeaders: []string{"Origin", "Authorization", "Content-Type"},
ExposeHeaders: []string{"Content-Length"}, ExposeHeaders: []string{"Content-Length"},
@@ -57,7 +66,7 @@ func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provide
}) })
// Auth Handler // Auth Handler
authHandler := auth.NewHandler(c, t, l, cfg) authHandler := auth.NewHandler(c, t, l, cfg, db)
// api := r.Group("/v") // api := r.Group("/v")
authGroup := r.Group("/auth") authGroup := r.Group("/auth")
{ {
@@ -70,7 +79,7 @@ func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provide
} }
// Auth Middleware // Auth Middleware
authMiddleware := middleware.NewAuthMiddleware(c, t, cfg) authMiddleware := middleware.NewAuthMiddleware(c, t, cfg, db, l)
// Init Storage Provider (S3) // Init Storage Provider (S3)
s3Provider, err := storage.NewS3Provider(cfg) s3Provider, err := storage.NewS3Provider(cfg)
@@ -81,22 +90,47 @@ func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provide
} }
// Handlers // Handlers
planHandler := plan.NewHandler(l, cfg) planHandler := plan.NewHandler(l, cfg, db)
paymentHandler := payment.NewHandler(l, cfg) paymentHandler := payment.NewHandler(l, cfg, db)
videoHandler := video.NewHandler(l, cfg, s3Provider) usageHandler := usage.NewHandler(l, db)
videoHandler := video.NewHandler(l, cfg, db, s3Provider)
preferencesHandler := preferences.NewHandler(l, db)
notificationHandler := notifications.NewHandler(l, db)
domainHandler := domains.NewHandler(l, db)
adTemplateHandler := adtemplates.NewHandler(l, db)
// Example protected group // Example protected group
protected := r.Group("") protected := r.Group("")
protected.Use(authMiddleware.Handle()) protected.Use(authMiddleware.Handle())
{ {
protected.GET("/me", func(c *gin.Context) { protected.GET("/me", authHandler.GetMe)
user, _ := c.Get("user") protected.PUT("/me", authHandler.UpdateMe)
response.Success(c, gin.H{"user": user}) protected.DELETE("/me", authHandler.DeleteMe)
// c.JSON(http.StatusOK, gin.H{ protected.POST("/me/clear-data", authHandler.ClearMyData)
// "user": user,
// })
})
protected.POST("/auth/logout", authHandler.Logout) protected.POST("/auth/logout", authHandler.Logout)
protected.POST("/auth/change-password", authHandler.ChangePassword)
preferences := protected.Group("/settings/preferences")
preferences.GET("", preferencesHandler.GetPreferences)
preferences.PUT("", preferencesHandler.UpdatePreferences)
notifications := protected.Group("/notifications")
notifications.GET("", notificationHandler.ListNotifications)
notifications.POST("/:id/read", notificationHandler.MarkRead)
notifications.POST("/read-all", notificationHandler.MarkAllRead)
notifications.DELETE("/:id", notificationHandler.DeleteNotification)
notifications.DELETE("", notificationHandler.ClearNotifications)
domains := protected.Group("/domains")
domains.GET("", domainHandler.ListDomains)
domains.POST("", domainHandler.CreateDomain)
domains.DELETE("/:id", domainHandler.DeleteDomain)
adTemplates := protected.Group("/ad-templates")
adTemplates.GET("", adTemplateHandler.ListTemplates)
adTemplates.POST("", adTemplateHandler.CreateTemplate)
adTemplates.PUT("/:id", adTemplateHandler.UpdateTemplate)
adTemplates.DELETE("/:id", adTemplateHandler.DeleteTemplate)
// Plans // Plans
plans := protected.Group("/plans") plans := protected.Group("/plans")
@@ -105,6 +139,12 @@ func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provide
// Payments // Payments
payments := protected.Group("/payments") payments := protected.Group("/payments")
payments.POST("", paymentHandler.CreatePayment) payments.POST("", paymentHandler.CreatePayment)
payments.GET("/history", paymentHandler.ListPaymentHistory)
payments.GET("/:id/invoice", paymentHandler.DownloadInvoice)
wallet := protected.Group("/wallet")
wallet.POST("/topups", paymentHandler.TopupWallet)
protected.GET("/usage", usageHandler.GetUsage)
// Videos // Videos
video := protected.Group("/videos") video := protected.Group("/videos")
@@ -112,7 +152,68 @@ func SetupRouter(cfg *config.Config, db *gorm.DB, c cache.Cache, t token.Provide
video.POST("", videoHandler.CreateVideo) video.POST("", videoHandler.CreateVideo)
video.GET("", videoHandler.ListVideos) video.GET("", videoHandler.ListVideos)
video.GET("/:id", videoHandler.GetVideo) video.GET("/:id", videoHandler.GetVideo)
video.PUT("/:id", videoHandler.UpdateVideo)
video.DELETE("/:id", videoHandler.DeleteVideo)
} }
return r renderModule, err := videoruntime.NewModule(context.Background(), cfg, db, c, t, l)
if err != nil {
return nil, nil, err
}
r.Use(videoruntime.MetricsMiddleware())
r.GET("/health/live", renderModule.HandleLive)
r.GET("/health/ready", renderModule.HandleReady)
r.GET("/health/detailed", renderModule.HandleDetailed)
r.GET("/metrics", renderModule.MetricsHandler())
// Admin routes — require auth + admin role
adminHandler := admin.NewHandler(l, db, renderModule)
adminGroup := r.Group("/admin")
adminGroup.Use(authMiddleware.Handle())
adminGroup.Use(middleware.RequireAdmin())
{
adminGroup.GET("/dashboard", adminHandler.Dashboard)
adminGroup.GET("/users", adminHandler.ListUsers)
adminGroup.POST("/users", adminHandler.CreateUser)
adminGroup.GET("/users/:id", adminHandler.GetUser)
adminGroup.PUT("/users/:id", adminHandler.UpdateUser)
adminGroup.PUT("/users/:id/role", adminHandler.UpdateUserRole)
adminGroup.DELETE("/users/:id", adminHandler.DeleteUser)
adminGroup.GET("/videos", adminHandler.ListVideos)
adminGroup.POST("/videos", adminHandler.CreateVideo)
adminGroup.GET("/videos/:id", adminHandler.GetVideo)
adminGroup.PUT("/videos/:id", adminHandler.UpdateVideo)
adminGroup.DELETE("/videos/:id", adminHandler.DeleteVideo)
adminGroup.GET("/payments", adminHandler.ListPayments)
adminGroup.POST("/payments", adminHandler.CreatePayment)
adminGroup.GET("/payments/:id", adminHandler.GetPayment)
adminGroup.PUT("/payments/:id", adminHandler.UpdatePayment)
adminGroup.GET("/plans", adminHandler.ListPlans)
adminGroup.POST("/plans", adminHandler.CreatePlan)
adminGroup.PUT("/plans/:id", adminHandler.UpdatePlan)
adminGroup.DELETE("/plans/:id", adminHandler.DeletePlan)
adminGroup.GET("/ad-templates", adminHandler.ListAdTemplates)
adminGroup.POST("/ad-templates", adminHandler.CreateAdTemplate)
adminGroup.GET("/ad-templates/:id", adminHandler.GetAdTemplate)
adminGroup.PUT("/ad-templates/:id", adminHandler.UpdateAdTemplate)
adminGroup.DELETE("/ad-templates/:id", adminHandler.DeleteAdTemplate)
adminGroup.GET("/jobs", adminHandler.ListJobs)
adminGroup.POST("/jobs", adminHandler.CreateJob)
adminGroup.GET("/jobs/:id", adminHandler.GetJob)
adminGroup.GET("/jobs/:id/logs", adminHandler.GetJobLogs)
adminGroup.POST("/jobs/:id/cancel", adminHandler.CancelJob)
adminGroup.POST("/jobs/:id/retry", adminHandler.RetryJob)
adminGroup.GET("/agents", adminHandler.ListAgents)
adminGroup.POST("/agents/:id/restart", adminHandler.RestartAgent)
adminGroup.POST("/agents/:id/update", adminHandler.UpdateAgent)
}
return r, renderModule, nil
} }

View File

@@ -12,13 +12,30 @@ type Config struct {
Redis RedisConfig Redis RedisConfig
JWT JWTConfig JWT JWTConfig
Google GoogleConfig Google GoogleConfig
Frontend FrontendConfig
CORS CORSConfig
Email EmailConfig Email EmailConfig
AWS AWSConfig AWS AWSConfig
Render RenderConfig
Internal InternalAuthConfig
} }
type ServerConfig struct { type ServerConfig struct {
Port string `mapstructure:"port"` Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"` // e.g., "debug", "release" GRPCPort string `mapstructure:"grpc_port"`
Mode string `mapstructure:"mode"` // e.g., "debug", "release"
}
type RenderConfig struct {
AgentSecret string `mapstructure:"agent_secret"`
EnableMetrics bool `mapstructure:"enable_metrics"`
EnableTracing bool `mapstructure:"enable_tracing"`
OTLPEndpoint string `mapstructure:"otlp_endpoint"`
ServiceName string `mapstructure:"service_name"`
}
type InternalAuthConfig struct {
Marker string `mapstructure:"marker"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {
@@ -36,9 +53,19 @@ type JWTConfig struct {
} }
type GoogleConfig struct { type GoogleConfig struct {
ClientID string `mapstructure:"client_id"` ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"` ClientSecret string `mapstructure:"client_secret"`
RedirectURL string `mapstructure:"redirect_url"` RedirectURL string `mapstructure:"redirect_url"`
StateTTLMinute int `mapstructure:"state_ttl_minutes"`
}
type FrontendConfig struct {
BaseURL string `mapstructure:"base_url"`
GoogleAuthFinalizePath string `mapstructure:"google_auth_finalize_path"`
}
type CORSConfig struct {
AllowOrigins []string `mapstructure:"allow_origins"`
} }
type EmailConfig struct { type EmailConfig struct {
@@ -60,8 +87,16 @@ func LoadConfig() (*Config, error) {
// Set defaults // Set defaults
v.SetDefault("server.port", "8080") v.SetDefault("server.port", "8080")
v.SetDefault("server.grpc_port", "9000")
v.SetDefault("server.mode", "debug") v.SetDefault("server.mode", "debug")
v.SetDefault("redis.db", 0) v.SetDefault("redis.db", 0)
v.SetDefault("render.enable_metrics", true)
v.SetDefault("render.enable_tracing", false)
v.SetDefault("render.service_name", "stream-api-render")
v.SetDefault("google.state_ttl_minutes", 10)
v.SetDefault("frontend.google_auth_finalize_path", "/auth/google/finalize")
v.SetDefault("internal.marker", "")
v.SetDefault("cors.allow_origins", []string{"http://localhost:5173", "http://localhost:8080", "http://localhost:8081"})
// Environment variable settings // Environment variable settings
v.SetEnvPrefix("APP") v.SetEnvPrefix("APP")

View File

@@ -0,0 +1,32 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAdTemplate = "ad_templates"
// AdTemplate mapped from table <ad_templates>
type AdTemplate struct {
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
UserID string `gorm:"column:user_id;type:uuid;not null;index:idx_ad_templates_user_id,priority:1" json:"user_id"`
Name string `gorm:"column:name;type:text;not null" json:"name"`
Description *string `gorm:"column:description;type:text" json:"description"`
VastTagURL string `gorm:"column:vast_tag_url;type:text;not null" json:"vast_tag_url"`
AdFormat *string `gorm:"column:ad_format;type:character varying(50);not null;default:pre-roll" json:"ad_format"`
Duration *int64 `gorm:"column:duration;type:bigint" json:"duration"`
IsActive *bool `gorm:"column:is_active;type:boolean;not null;default:true" json:"is_active"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
IsDefault bool `gorm:"column:is_default;type:boolean;not null" json:"is_default"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName AdTemplate's table name
func (*AdTemplate) TableName() string {
return TableNameAdTemplate
}

View File

@@ -0,0 +1,26 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameDomain = "domains"
// Domain mapped from table <domains>
type Domain struct {
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
UserID string `gorm:"column:user_id;type:uuid;not null;index:idx_domains_user_id,priority:1" json:"user_id"`
Name string `gorm:"column:name;type:text;not null" json:"name"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName Domain's table name
func (*Domain) TableName() string {
return TableNameDomain
}

View File

@@ -0,0 +1,138 @@
package model
import (
"context"
"errors"
"strings"
"time"
"gorm.io/gorm"
)
const (
defaultPreferenceLanguage = "en"
defaultPreferenceLocale = "en"
)
func DefaultUserPreference(userID string) *UserPreference {
return &UserPreference{
UserID: userID,
Language: StringPtr(defaultPreferenceLanguage),
Locale: StringPtr(defaultPreferenceLocale),
EmailNotifications: BoolPtr(true),
PushNotifications: BoolPtr(true),
MarketingNotifications: false,
TelegramNotifications: false,
Autoplay: false,
Loop: false,
Muted: false,
ShowControls: BoolPtr(true),
Pip: BoolPtr(true),
Airplay: BoolPtr(true),
Chromecast: BoolPtr(true),
}
}
func FindOrCreateUserPreference(ctx context.Context, db *gorm.DB, userID string) (*UserPreference, error) {
var pref UserPreference
if err := db.WithContext(ctx).Where("user_id = ?", userID).First(&pref).Error; err == nil {
normalizeUserPreferenceDefaults(&pref)
return &pref, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
pref = *DefaultUserPreference(userID)
if err := db.WithContext(ctx).Create(&pref).Error; err != nil {
return nil, err
}
return &pref, nil
}
func GetWalletBalance(ctx context.Context, db *gorm.DB, userID string) (float64, error) {
var balance float64
if err := db.WithContext(ctx).
Model(&WalletTransaction{}).
Where("user_id = ?", userID).
Select("COALESCE(SUM(amount), 0)").
Scan(&balance).Error; err != nil {
return 0, err
}
return balance, nil
}
func GetLatestPlanSubscription(ctx context.Context, db *gorm.DB, userID string) (*PlanSubscription, error) {
userID = strings.TrimSpace(userID)
if userID == "" {
return nil, gorm.ErrRecordNotFound
}
var subscription PlanSubscription
if err := db.WithContext(ctx).
Where("user_id = ?", userID).
Order("created_at DESC").
Order("id DESC").
First(&subscription).Error; err != nil {
return nil, err
}
return &subscription, nil
}
func IsSubscriptionExpiringSoon(expiresAt time.Time, now time.Time) bool {
if expiresAt.IsZero() || !expiresAt.After(now) {
return false
}
return expiresAt.Sub(now) <= 7*24*time.Hour
}
func normalizeUserPreferenceDefaults(pref *UserPreference) {
if pref == nil {
return
}
if strings.TrimSpace(StringValue(pref.Language)) == "" {
pref.Language = StringPtr(defaultPreferenceLanguage)
}
if strings.TrimSpace(StringValue(pref.Locale)) == "" {
locale := StringValue(pref.Language)
if strings.TrimSpace(locale) == "" {
locale = defaultPreferenceLocale
}
pref.Locale = StringPtr(locale)
}
if pref.EmailNotifications == nil {
pref.EmailNotifications = BoolPtr(true)
}
if pref.PushNotifications == nil {
pref.PushNotifications = BoolPtr(true)
}
if pref.ShowControls == nil {
pref.ShowControls = BoolPtr(true)
}
if pref.Pip == nil {
pref.Pip = BoolPtr(true)
}
if pref.Airplay == nil {
pref.Airplay = BoolPtr(true)
}
if pref.Chromecast == nil {
pref.Chromecast = BoolPtr(true)
}
}
func StringPtr(value string) *string {
v := value
return &v
}
func BoolPtr(value bool) *bool {
v := value
return &v
}
func StringValue(value *string) string {
if value == nil {
return ""
}
return *value
}

View File

@@ -0,0 +1,37 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameJob = "jobs"
// Job mapped from table <jobs>
type Job struct {
ID string `gorm:"column:id;type:text;primaryKey" json:"id"`
Status *string `gorm:"column:status;type:text" json:"status"`
Priority *int64 `gorm:"column:priority;type:bigint;index:idx_jobs_priority,priority:1" json:"priority"`
InputURL *string `gorm:"column:input_url;type:text" json:"input_url"`
OutputURL *string `gorm:"column:output_url;type:text" json:"output_url"`
TotalDuration *int64 `gorm:"column:total_duration;type:bigint" json:"total_duration"`
CurrentTime *int64 `gorm:"column:current_time;type:bigint" json:"current_time"`
Progress *float64 `gorm:"column:progress;type:numeric" json:"progress"`
AgentID *int64 `gorm:"column:agent_id;type:bigint" json:"agent_id"`
Logs *string `gorm:"column:logs;type:text" json:"logs"`
Config *string `gorm:"column:config;type:text" json:"config"`
Cancelled *bool `gorm:"column:cancelled;type:boolean" json:"cancelled"`
RetryCount *int64 `gorm:"column:retry_count;type:bigint" json:"retry_count"`
MaxRetries *int64 `gorm:"column:max_retries;type:bigint;default:3" json:"max_retries"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
Version *int64 `gorm:"column:version;type:bigint;version" json:"-"`
}
// TableName Job's table name
func (*Job) TableName() string {
return TableNameJob
}

View File

@@ -0,0 +1,32 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameNotification = "notifications"
// Notification mapped from table <notifications>
type Notification struct {
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
UserID string `gorm:"column:user_id;type:uuid;not null;index:idx_notifications_user_id,priority:1" json:"user_id"`
Type string `gorm:"column:type;type:character varying(50);not null" json:"type"`
Title string `gorm:"column:title;type:text;not null" json:"title"`
Message string `gorm:"column:message;type:text;not null" json:"message"`
Metadata *string `gorm:"column:metadata;type:text" json:"metadata"`
ActionURL *string `gorm:"column:action_url;type:text" json:"action_url"`
ActionLabel *string `gorm:"column:action_label;type:text" json:"action_label"`
IsRead bool `gorm:"column:is_read;type:boolean;not null" json:"is_read"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName Notification's table name
func (*Notification) TableName() string {
return TableNameNotification
}

View File

@@ -4,22 +4,26 @@
package model package model
import (
"github.com/lib/pq"
)
const TableNamePlan = "plan" const TableNamePlan = "plan"
// Plan mapped from table <plan> // Plan mapped from table <plan>
type Plan struct { type Plan struct {
ID string `gorm:"column:id;type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` ID string `gorm:"column:id;type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Name string `gorm:"column:name;type:text;not null" json:"name"` Name string `gorm:"column:name;type:text;not null" json:"name"`
Description *string `gorm:"column:description;type:text" json:"description"` Description *string `gorm:"column:description;type:text" json:"description"`
Price float64 `gorm:"column:price;type:numeric(65,30);not null" json:"price"` Price float64 `gorm:"column:price;type:numeric(65,30);not null" json:"price"`
Cycle string `gorm:"column:cycle;type:character varying(20);not null" json:"cycle"` Cycle string `gorm:"column:cycle;type:character varying(20);not null" json:"cycle"`
StorageLimit int64 `gorm:"column:storage_limit;type:bigint;not null" json:"storage_limit"` StorageLimit int64 `gorm:"column:storage_limit;type:bigint;not null" json:"storage_limit"`
UploadLimit int32 `gorm:"column:upload_limit;type:integer;not null" json:"upload_limit"` UploadLimit int32 `gorm:"column:upload_limit;type:integer;not null" json:"upload_limit"`
DurationLimit int32 `gorm:"column:duration_limit;type:integer;not null" json:"duration_limit"` DurationLimit int32 `gorm:"column:duration_limit;type:integer;not null" json:"duration_limit"`
QualityLimit string `gorm:"column:quality_limit;type:text;not null" json:"quality_limit"` QualityLimit string `gorm:"column:quality_limit;type:text;not null" json:"quality_limit"`
Features *string `gorm:"column:features;type:text[]" json:"features"` Features pq.StringArray `gorm:"column:features;type:text[]" json:"features"`
IsActive *bool `gorm:"column:is_active;type:boolean;not null;default:true" json:"is_active"` IsActive *bool `gorm:"column:is_active;type:boolean;not null;default:true" json:"is_active"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"` Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
} }
// TableName Plan's table name // TableName Plan's table name

View File

@@ -0,0 +1,36 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNamePlanSubscription = "plan_subscriptions"
// PlanSubscription mapped from table <plan_subscriptions>
type PlanSubscription struct {
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
UserID string `gorm:"column:user_id;type:uuid;not null;index:idx_plan_subscriptions_user_id,priority:1" json:"user_id"`
PaymentID string `gorm:"column:payment_id;type:uuid;not null;index:idx_plan_subscriptions_payment_id,priority:1" json:"payment_id"`
PlanID string `gorm:"column:plan_id;type:uuid;not null;index:idx_plan_subscriptions_plan_id,priority:1" json:"plan_id"`
TermMonths int32 `gorm:"column:term_months;type:integer;not null" json:"term_months"`
PaymentMethod string `gorm:"column:payment_method;type:character varying(20);not null" json:"payment_method"`
WalletAmount float64 `gorm:"column:wallet_amount;type:numeric(65,30);not null" json:"wallet_amount"`
TopupAmount float64 `gorm:"column:topup_amount;type:numeric(65,30);not null" json:"topup_amount"`
StartedAt time.Time `gorm:"column:started_at;type:timestamp with time zone;not null" json:"started_at"`
ExpiresAt time.Time `gorm:"column:expires_at;type:timestamp with time zone;not null;index:idx_plan_subscriptions_expires_at,priority:1" json:"expires_at"`
Reminder7DSentAt *time.Time `gorm:"column:reminder_7d_sent_at;type:timestamp with time zone" json:"reminder_7d_sent_at"`
Reminder3DSentAt *time.Time `gorm:"column:reminder_3d_sent_at;type:timestamp with time zone" json:"reminder_3d_sent_at"`
Reminder1DSentAt *time.Time `gorm:"column:reminder_1d_sent_at;type:timestamp with time zone" json:"reminder_1d_sent_at"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName PlanSubscription's table name
func (*PlanSubscription) TableName() string {
return TableNamePlanSubscription
}

View File

@@ -0,0 +1,38 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameUserPreference = "user_preferences"
// UserPreference mapped from table <user_preferences>
type UserPreference struct {
UserID string `gorm:"column:user_id;type:uuid;primaryKey" json:"user_id"`
Language *string `gorm:"column:language;type:text;not null;default:en" json:"language"`
Locale *string `gorm:"column:locale;type:text;not null;default:en" json:"locale"`
EmailNotifications *bool `gorm:"column:email_notifications;type:boolean;not null;default:true" json:"email_notifications"`
PushNotifications *bool `gorm:"column:push_notifications;type:boolean;not null;default:true" json:"push_notifications"`
MarketingNotifications bool `gorm:"column:marketing_notifications;type:boolean;not null" json:"marketing_notifications"`
TelegramNotifications bool `gorm:"column:telegram_notifications;type:boolean;not null" json:"telegram_notifications"`
Autoplay bool `gorm:"column:autoplay;type:boolean;not null" json:"autoplay"`
Loop bool `gorm:"column:loop;type:boolean;not null" json:"loop"`
Muted bool `gorm:"column:muted;type:boolean;not null" json:"muted"`
ShowControls *bool `gorm:"column:show_controls;type:boolean;not null;default:true" json:"show_controls"`
Pip *bool `gorm:"column:pip;type:boolean;not null;default:true" json:"pip"`
Airplay *bool `gorm:"column:airplay;type:boolean;not null;default:true" json:"airplay"`
Chromecast *bool `gorm:"column:chromecast;type:boolean;not null;default:true" json:"chromecast"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
EncrytionM3u8 bool `gorm:"column:encrytion_m3u8;type:boolean;not null" json:"encrytion_m3u8"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName UserPreference's table name
func (*UserPreference) TableName() string {
return TableNameUserPreference
}

View File

@@ -0,0 +1,29 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameVideoAdConfig = "video_ad_configs"
// VideoAdConfig mapped from table <video_ad_configs>
type VideoAdConfig struct {
VideoID string `gorm:"column:video_id;type:uuid;primaryKey" json:"video_id"`
UserID string `gorm:"column:user_id;type:uuid;not null;index:idx_video_ad_configs_user_id,priority:1" json:"user_id"`
AdTemplateID string `gorm:"column:ad_template_id;type:uuid;not null" json:"ad_template_id"`
VastTagURL string `gorm:"column:vast_tag_url;type:text;not null" json:"vast_tag_url"`
AdFormat *string `gorm:"column:ad_format;type:character varying(50);not null;default:pre-roll" json:"ad_format"`
Duration *int64 `gorm:"column:duration;type:bigint" json:"duration"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName VideoAdConfig's table name
func (*VideoAdConfig) TableName() string {
return TableNameVideoAdConfig
}

View File

@@ -0,0 +1,32 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameWalletTransaction = "wallet_transactions"
// WalletTransaction mapped from table <wallet_transactions>
type WalletTransaction struct {
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
UserID string `gorm:"column:user_id;type:uuid;not null;index:idx_wallet_transactions_user_id,priority:1" json:"user_id"`
Type string `gorm:"column:type;type:character varying(50);not null" json:"type"`
Amount float64 `gorm:"column:amount;type:numeric(65,30);not null" json:"amount"`
Currency *string `gorm:"column:currency;type:text;not null;default:USD" json:"currency"`
Note *string `gorm:"column:note;type:text" json:"note"`
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp with time zone" json:"created_at"`
UpdatedAt *time.Time `gorm:"column:updated_at;type:timestamp with time zone" json:"updated_at"`
PaymentID *string `gorm:"column:payment_id;type:uuid;index:idx_wallet_transactions_payment_id,priority:1" json:"payment_id"`
PlanID *string `gorm:"column:plan_id;type:uuid;index:idx_wallet_transactions_plan_id,priority:1" json:"plan_id"`
TermMonths *int32 `gorm:"column:term_months;type:integer" json:"term_months"`
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
}
// TableName WalletTransaction's table name
func (*WalletTransaction) TableName() string {
return TableNameWalletTransaction
}

View File

@@ -0,0 +1,437 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newAdTemplate(db *gorm.DB, opts ...gen.DOOption) adTemplate {
_adTemplate := adTemplate{}
_adTemplate.adTemplateDo.UseDB(db, opts...)
_adTemplate.adTemplateDo.UseModel(&model.AdTemplate{})
tableName := _adTemplate.adTemplateDo.TableName()
_adTemplate.ALL = field.NewAsterisk(tableName)
_adTemplate.ID = field.NewString(tableName, "id")
_adTemplate.UserID = field.NewString(tableName, "user_id")
_adTemplate.Name = field.NewString(tableName, "name")
_adTemplate.Description = field.NewString(tableName, "description")
_adTemplate.VastTagURL = field.NewString(tableName, "vast_tag_url")
_adTemplate.AdFormat = field.NewString(tableName, "ad_format")
_adTemplate.Duration = field.NewInt64(tableName, "duration")
_adTemplate.IsActive = field.NewBool(tableName, "is_active")
_adTemplate.CreatedAt = field.NewTime(tableName, "created_at")
_adTemplate.UpdatedAt = field.NewTime(tableName, "updated_at")
_adTemplate.IsDefault = field.NewBool(tableName, "is_default")
_adTemplate.Version = field.NewInt64(tableName, "version")
_adTemplate.fillFieldMap()
return _adTemplate
}
type adTemplate struct {
adTemplateDo adTemplateDo
ALL field.Asterisk
ID field.String
UserID field.String
Name field.String
Description field.String
VastTagURL field.String
AdFormat field.String
Duration field.Int64
IsActive field.Bool
CreatedAt field.Time
UpdatedAt field.Time
IsDefault field.Bool
Version field.Int64
fieldMap map[string]field.Expr
}
func (a adTemplate) Table(newTableName string) *adTemplate {
a.adTemplateDo.UseTable(newTableName)
return a.updateTableName(newTableName)
}
func (a adTemplate) As(alias string) *adTemplate {
a.adTemplateDo.DO = *(a.adTemplateDo.As(alias).(*gen.DO))
return a.updateTableName(alias)
}
func (a *adTemplate) updateTableName(table string) *adTemplate {
a.ALL = field.NewAsterisk(table)
a.ID = field.NewString(table, "id")
a.UserID = field.NewString(table, "user_id")
a.Name = field.NewString(table, "name")
a.Description = field.NewString(table, "description")
a.VastTagURL = field.NewString(table, "vast_tag_url")
a.AdFormat = field.NewString(table, "ad_format")
a.Duration = field.NewInt64(table, "duration")
a.IsActive = field.NewBool(table, "is_active")
a.CreatedAt = field.NewTime(table, "created_at")
a.UpdatedAt = field.NewTime(table, "updated_at")
a.IsDefault = field.NewBool(table, "is_default")
a.Version = field.NewInt64(table, "version")
a.fillFieldMap()
return a
}
func (a *adTemplate) WithContext(ctx context.Context) IAdTemplateDo {
return a.adTemplateDo.WithContext(ctx)
}
func (a adTemplate) TableName() string { return a.adTemplateDo.TableName() }
func (a adTemplate) Alias() string { return a.adTemplateDo.Alias() }
func (a adTemplate) Columns(cols ...field.Expr) gen.Columns { return a.adTemplateDo.Columns(cols...) }
func (a *adTemplate) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := a.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (a *adTemplate) fillFieldMap() {
a.fieldMap = make(map[string]field.Expr, 12)
a.fieldMap["id"] = a.ID
a.fieldMap["user_id"] = a.UserID
a.fieldMap["name"] = a.Name
a.fieldMap["description"] = a.Description
a.fieldMap["vast_tag_url"] = a.VastTagURL
a.fieldMap["ad_format"] = a.AdFormat
a.fieldMap["duration"] = a.Duration
a.fieldMap["is_active"] = a.IsActive
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["updated_at"] = a.UpdatedAt
a.fieldMap["is_default"] = a.IsDefault
a.fieldMap["version"] = a.Version
}
func (a adTemplate) clone(db *gorm.DB) adTemplate {
a.adTemplateDo.ReplaceConnPool(db.Statement.ConnPool)
return a
}
func (a adTemplate) replaceDB(db *gorm.DB) adTemplate {
a.adTemplateDo.ReplaceDB(db)
return a
}
type adTemplateDo struct{ gen.DO }
type IAdTemplateDo interface {
gen.SubQuery
Debug() IAdTemplateDo
WithContext(ctx context.Context) IAdTemplateDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IAdTemplateDo
WriteDB() IAdTemplateDo
As(alias string) gen.Dao
Session(config *gorm.Session) IAdTemplateDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IAdTemplateDo
Not(conds ...gen.Condition) IAdTemplateDo
Or(conds ...gen.Condition) IAdTemplateDo
Select(conds ...field.Expr) IAdTemplateDo
Where(conds ...gen.Condition) IAdTemplateDo
Order(conds ...field.Expr) IAdTemplateDo
Distinct(cols ...field.Expr) IAdTemplateDo
Omit(cols ...field.Expr) IAdTemplateDo
Join(table schema.Tabler, on ...field.Expr) IAdTemplateDo
LeftJoin(table schema.Tabler, on ...field.Expr) IAdTemplateDo
RightJoin(table schema.Tabler, on ...field.Expr) IAdTemplateDo
Group(cols ...field.Expr) IAdTemplateDo
Having(conds ...gen.Condition) IAdTemplateDo
Limit(limit int) IAdTemplateDo
Offset(offset int) IAdTemplateDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IAdTemplateDo
Unscoped() IAdTemplateDo
Create(values ...*model.AdTemplate) error
CreateInBatches(values []*model.AdTemplate, batchSize int) error
Save(values ...*model.AdTemplate) error
First() (*model.AdTemplate, error)
Take() (*model.AdTemplate, error)
Last() (*model.AdTemplate, error)
Find() ([]*model.AdTemplate, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.AdTemplate, err error)
FindInBatches(result *[]*model.AdTemplate, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.AdTemplate) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IAdTemplateDo
Assign(attrs ...field.AssignExpr) IAdTemplateDo
Joins(fields ...field.RelationField) IAdTemplateDo
Preload(fields ...field.RelationField) IAdTemplateDo
FirstOrInit() (*model.AdTemplate, error)
FirstOrCreate() (*model.AdTemplate, error)
FindByPage(offset int, limit int) (result []*model.AdTemplate, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IAdTemplateDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (a adTemplateDo) Debug() IAdTemplateDo {
return a.withDO(a.DO.Debug())
}
func (a adTemplateDo) WithContext(ctx context.Context) IAdTemplateDo {
return a.withDO(a.DO.WithContext(ctx))
}
func (a adTemplateDo) ReadDB() IAdTemplateDo {
return a.Clauses(dbresolver.Read)
}
func (a adTemplateDo) WriteDB() IAdTemplateDo {
return a.Clauses(dbresolver.Write)
}
func (a adTemplateDo) Session(config *gorm.Session) IAdTemplateDo {
return a.withDO(a.DO.Session(config))
}
func (a adTemplateDo) Clauses(conds ...clause.Expression) IAdTemplateDo {
return a.withDO(a.DO.Clauses(conds...))
}
func (a adTemplateDo) Returning(value interface{}, columns ...string) IAdTemplateDo {
return a.withDO(a.DO.Returning(value, columns...))
}
func (a adTemplateDo) Not(conds ...gen.Condition) IAdTemplateDo {
return a.withDO(a.DO.Not(conds...))
}
func (a adTemplateDo) Or(conds ...gen.Condition) IAdTemplateDo {
return a.withDO(a.DO.Or(conds...))
}
func (a adTemplateDo) Select(conds ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.Select(conds...))
}
func (a adTemplateDo) Where(conds ...gen.Condition) IAdTemplateDo {
return a.withDO(a.DO.Where(conds...))
}
func (a adTemplateDo) Order(conds ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.Order(conds...))
}
func (a adTemplateDo) Distinct(cols ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.Distinct(cols...))
}
func (a adTemplateDo) Omit(cols ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.Omit(cols...))
}
func (a adTemplateDo) Join(table schema.Tabler, on ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.Join(table, on...))
}
func (a adTemplateDo) LeftJoin(table schema.Tabler, on ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.LeftJoin(table, on...))
}
func (a adTemplateDo) RightJoin(table schema.Tabler, on ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.RightJoin(table, on...))
}
func (a adTemplateDo) Group(cols ...field.Expr) IAdTemplateDo {
return a.withDO(a.DO.Group(cols...))
}
func (a adTemplateDo) Having(conds ...gen.Condition) IAdTemplateDo {
return a.withDO(a.DO.Having(conds...))
}
func (a adTemplateDo) Limit(limit int) IAdTemplateDo {
return a.withDO(a.DO.Limit(limit))
}
func (a adTemplateDo) Offset(offset int) IAdTemplateDo {
return a.withDO(a.DO.Offset(offset))
}
func (a adTemplateDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IAdTemplateDo {
return a.withDO(a.DO.Scopes(funcs...))
}
func (a adTemplateDo) Unscoped() IAdTemplateDo {
return a.withDO(a.DO.Unscoped())
}
func (a adTemplateDo) Create(values ...*model.AdTemplate) error {
if len(values) == 0 {
return nil
}
return a.DO.Create(values)
}
func (a adTemplateDo) CreateInBatches(values []*model.AdTemplate, batchSize int) error {
return a.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (a adTemplateDo) Save(values ...*model.AdTemplate) error {
if len(values) == 0 {
return nil
}
return a.DO.Save(values)
}
func (a adTemplateDo) First() (*model.AdTemplate, error) {
if result, err := a.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.AdTemplate), nil
}
}
func (a adTemplateDo) Take() (*model.AdTemplate, error) {
if result, err := a.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.AdTemplate), nil
}
}
func (a adTemplateDo) Last() (*model.AdTemplate, error) {
if result, err := a.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.AdTemplate), nil
}
}
func (a adTemplateDo) Find() ([]*model.AdTemplate, error) {
result, err := a.DO.Find()
return result.([]*model.AdTemplate), err
}
func (a adTemplateDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.AdTemplate, err error) {
buf := make([]*model.AdTemplate, 0, batchSize)
err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (a adTemplateDo) FindInBatches(result *[]*model.AdTemplate, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return a.DO.FindInBatches(result, batchSize, fc)
}
func (a adTemplateDo) Attrs(attrs ...field.AssignExpr) IAdTemplateDo {
return a.withDO(a.DO.Attrs(attrs...))
}
func (a adTemplateDo) Assign(attrs ...field.AssignExpr) IAdTemplateDo {
return a.withDO(a.DO.Assign(attrs...))
}
func (a adTemplateDo) Joins(fields ...field.RelationField) IAdTemplateDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Joins(_f))
}
return &a
}
func (a adTemplateDo) Preload(fields ...field.RelationField) IAdTemplateDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Preload(_f))
}
return &a
}
func (a adTemplateDo) FirstOrInit() (*model.AdTemplate, error) {
if result, err := a.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.AdTemplate), nil
}
}
func (a adTemplateDo) FirstOrCreate() (*model.AdTemplate, error) {
if result, err := a.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.AdTemplate), nil
}
}
func (a adTemplateDo) FindByPage(offset int, limit int) (result []*model.AdTemplate, count int64, err error) {
result, err = a.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = a.Offset(-1).Limit(-1).Count()
return
}
func (a adTemplateDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = a.Count()
if err != nil {
return
}
err = a.Offset(offset).Limit(limit).Scan(result)
return
}
func (a adTemplateDo) Scan(result interface{}) (err error) {
return a.DO.Scan(result)
}
func (a adTemplateDo) Delete(models ...*model.AdTemplate) (result gen.ResultInfo, err error) {
return a.DO.Delete(models)
}
func (a *adTemplateDo) withDO(do gen.Dao) *adTemplateDo {
a.DO = *do.(*gen.DO)
return a
}

View File

@@ -0,0 +1,411 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newDomain(db *gorm.DB, opts ...gen.DOOption) domain {
_domain := domain{}
_domain.domainDo.UseDB(db, opts...)
_domain.domainDo.UseModel(&model.Domain{})
tableName := _domain.domainDo.TableName()
_domain.ALL = field.NewAsterisk(tableName)
_domain.ID = field.NewString(tableName, "id")
_domain.UserID = field.NewString(tableName, "user_id")
_domain.Name = field.NewString(tableName, "name")
_domain.CreatedAt = field.NewTime(tableName, "created_at")
_domain.UpdatedAt = field.NewTime(tableName, "updated_at")
_domain.Version = field.NewInt64(tableName, "version")
_domain.fillFieldMap()
return _domain
}
type domain struct {
domainDo domainDo
ALL field.Asterisk
ID field.String
UserID field.String
Name field.String
CreatedAt field.Time
UpdatedAt field.Time
Version field.Int64
fieldMap map[string]field.Expr
}
func (d domain) Table(newTableName string) *domain {
d.domainDo.UseTable(newTableName)
return d.updateTableName(newTableName)
}
func (d domain) As(alias string) *domain {
d.domainDo.DO = *(d.domainDo.As(alias).(*gen.DO))
return d.updateTableName(alias)
}
func (d *domain) updateTableName(table string) *domain {
d.ALL = field.NewAsterisk(table)
d.ID = field.NewString(table, "id")
d.UserID = field.NewString(table, "user_id")
d.Name = field.NewString(table, "name")
d.CreatedAt = field.NewTime(table, "created_at")
d.UpdatedAt = field.NewTime(table, "updated_at")
d.Version = field.NewInt64(table, "version")
d.fillFieldMap()
return d
}
func (d *domain) WithContext(ctx context.Context) IDomainDo { return d.domainDo.WithContext(ctx) }
func (d domain) TableName() string { return d.domainDo.TableName() }
func (d domain) Alias() string { return d.domainDo.Alias() }
func (d domain) Columns(cols ...field.Expr) gen.Columns { return d.domainDo.Columns(cols...) }
func (d *domain) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := d.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (d *domain) fillFieldMap() {
d.fieldMap = make(map[string]field.Expr, 6)
d.fieldMap["id"] = d.ID
d.fieldMap["user_id"] = d.UserID
d.fieldMap["name"] = d.Name
d.fieldMap["created_at"] = d.CreatedAt
d.fieldMap["updated_at"] = d.UpdatedAt
d.fieldMap["version"] = d.Version
}
func (d domain) clone(db *gorm.DB) domain {
d.domainDo.ReplaceConnPool(db.Statement.ConnPool)
return d
}
func (d domain) replaceDB(db *gorm.DB) domain {
d.domainDo.ReplaceDB(db)
return d
}
type domainDo struct{ gen.DO }
type IDomainDo interface {
gen.SubQuery
Debug() IDomainDo
WithContext(ctx context.Context) IDomainDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IDomainDo
WriteDB() IDomainDo
As(alias string) gen.Dao
Session(config *gorm.Session) IDomainDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IDomainDo
Not(conds ...gen.Condition) IDomainDo
Or(conds ...gen.Condition) IDomainDo
Select(conds ...field.Expr) IDomainDo
Where(conds ...gen.Condition) IDomainDo
Order(conds ...field.Expr) IDomainDo
Distinct(cols ...field.Expr) IDomainDo
Omit(cols ...field.Expr) IDomainDo
Join(table schema.Tabler, on ...field.Expr) IDomainDo
LeftJoin(table schema.Tabler, on ...field.Expr) IDomainDo
RightJoin(table schema.Tabler, on ...field.Expr) IDomainDo
Group(cols ...field.Expr) IDomainDo
Having(conds ...gen.Condition) IDomainDo
Limit(limit int) IDomainDo
Offset(offset int) IDomainDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IDomainDo
Unscoped() IDomainDo
Create(values ...*model.Domain) error
CreateInBatches(values []*model.Domain, batchSize int) error
Save(values ...*model.Domain) error
First() (*model.Domain, error)
Take() (*model.Domain, error)
Last() (*model.Domain, error)
Find() ([]*model.Domain, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Domain, err error)
FindInBatches(result *[]*model.Domain, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.Domain) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IDomainDo
Assign(attrs ...field.AssignExpr) IDomainDo
Joins(fields ...field.RelationField) IDomainDo
Preload(fields ...field.RelationField) IDomainDo
FirstOrInit() (*model.Domain, error)
FirstOrCreate() (*model.Domain, error)
FindByPage(offset int, limit int) (result []*model.Domain, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IDomainDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (d domainDo) Debug() IDomainDo {
return d.withDO(d.DO.Debug())
}
func (d domainDo) WithContext(ctx context.Context) IDomainDo {
return d.withDO(d.DO.WithContext(ctx))
}
func (d domainDo) ReadDB() IDomainDo {
return d.Clauses(dbresolver.Read)
}
func (d domainDo) WriteDB() IDomainDo {
return d.Clauses(dbresolver.Write)
}
func (d domainDo) Session(config *gorm.Session) IDomainDo {
return d.withDO(d.DO.Session(config))
}
func (d domainDo) Clauses(conds ...clause.Expression) IDomainDo {
return d.withDO(d.DO.Clauses(conds...))
}
func (d domainDo) Returning(value interface{}, columns ...string) IDomainDo {
return d.withDO(d.DO.Returning(value, columns...))
}
func (d domainDo) Not(conds ...gen.Condition) IDomainDo {
return d.withDO(d.DO.Not(conds...))
}
func (d domainDo) Or(conds ...gen.Condition) IDomainDo {
return d.withDO(d.DO.Or(conds...))
}
func (d domainDo) Select(conds ...field.Expr) IDomainDo {
return d.withDO(d.DO.Select(conds...))
}
func (d domainDo) Where(conds ...gen.Condition) IDomainDo {
return d.withDO(d.DO.Where(conds...))
}
func (d domainDo) Order(conds ...field.Expr) IDomainDo {
return d.withDO(d.DO.Order(conds...))
}
func (d domainDo) Distinct(cols ...field.Expr) IDomainDo {
return d.withDO(d.DO.Distinct(cols...))
}
func (d domainDo) Omit(cols ...field.Expr) IDomainDo {
return d.withDO(d.DO.Omit(cols...))
}
func (d domainDo) Join(table schema.Tabler, on ...field.Expr) IDomainDo {
return d.withDO(d.DO.Join(table, on...))
}
func (d domainDo) LeftJoin(table schema.Tabler, on ...field.Expr) IDomainDo {
return d.withDO(d.DO.LeftJoin(table, on...))
}
func (d domainDo) RightJoin(table schema.Tabler, on ...field.Expr) IDomainDo {
return d.withDO(d.DO.RightJoin(table, on...))
}
func (d domainDo) Group(cols ...field.Expr) IDomainDo {
return d.withDO(d.DO.Group(cols...))
}
func (d domainDo) Having(conds ...gen.Condition) IDomainDo {
return d.withDO(d.DO.Having(conds...))
}
func (d domainDo) Limit(limit int) IDomainDo {
return d.withDO(d.DO.Limit(limit))
}
func (d domainDo) Offset(offset int) IDomainDo {
return d.withDO(d.DO.Offset(offset))
}
func (d domainDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IDomainDo {
return d.withDO(d.DO.Scopes(funcs...))
}
func (d domainDo) Unscoped() IDomainDo {
return d.withDO(d.DO.Unscoped())
}
func (d domainDo) Create(values ...*model.Domain) error {
if len(values) == 0 {
return nil
}
return d.DO.Create(values)
}
func (d domainDo) CreateInBatches(values []*model.Domain, batchSize int) error {
return d.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (d domainDo) Save(values ...*model.Domain) error {
if len(values) == 0 {
return nil
}
return d.DO.Save(values)
}
func (d domainDo) First() (*model.Domain, error) {
if result, err := d.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.Domain), nil
}
}
func (d domainDo) Take() (*model.Domain, error) {
if result, err := d.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.Domain), nil
}
}
func (d domainDo) Last() (*model.Domain, error) {
if result, err := d.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.Domain), nil
}
}
func (d domainDo) Find() ([]*model.Domain, error) {
result, err := d.DO.Find()
return result.([]*model.Domain), err
}
func (d domainDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Domain, err error) {
buf := make([]*model.Domain, 0, batchSize)
err = d.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (d domainDo) FindInBatches(result *[]*model.Domain, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return d.DO.FindInBatches(result, batchSize, fc)
}
func (d domainDo) Attrs(attrs ...field.AssignExpr) IDomainDo {
return d.withDO(d.DO.Attrs(attrs...))
}
func (d domainDo) Assign(attrs ...field.AssignExpr) IDomainDo {
return d.withDO(d.DO.Assign(attrs...))
}
func (d domainDo) Joins(fields ...field.RelationField) IDomainDo {
for _, _f := range fields {
d = *d.withDO(d.DO.Joins(_f))
}
return &d
}
func (d domainDo) Preload(fields ...field.RelationField) IDomainDo {
for _, _f := range fields {
d = *d.withDO(d.DO.Preload(_f))
}
return &d
}
func (d domainDo) FirstOrInit() (*model.Domain, error) {
if result, err := d.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.Domain), nil
}
}
func (d domainDo) FirstOrCreate() (*model.Domain, error) {
if result, err := d.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.Domain), nil
}
}
func (d domainDo) FindByPage(offset int, limit int) (result []*model.Domain, count int64, err error) {
result, err = d.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = d.Offset(-1).Limit(-1).Count()
return
}
func (d domainDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = d.Count()
if err != nil {
return
}
err = d.Offset(offset).Limit(limit).Scan(result)
return
}
func (d domainDo) Scan(result interface{}) (err error) {
return d.DO.Scan(result)
}
func (d domainDo) Delete(models ...*model.Domain) (result gen.ResultInfo, err error) {
return d.DO.Delete(models)
}
func (d *domainDo) withDO(do gen.Dao) *domainDo {
d.DO = *do.(*gen.DO)
return d
}

View File

@@ -16,49 +16,89 @@ import (
) )
var ( var (
Q = new(Query) Q = new(Query)
Payment *payment AdTemplate *adTemplate
Plan *plan Domain *domain
User *user Job *job
Video *video Notification *notification
Payment *payment
Plan *plan
PlanSubscription *planSubscription
User *user
UserPreference *userPreference
Video *video
VideoAdConfig *videoAdConfig
WalletTransaction *walletTransaction
) )
func SetDefault(db *gorm.DB, opts ...gen.DOOption) { func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
*Q = *Use(db, opts...) *Q = *Use(db, opts...)
AdTemplate = &Q.AdTemplate
Domain = &Q.Domain
Job = &Q.Job
Notification = &Q.Notification
Payment = &Q.Payment Payment = &Q.Payment
Plan = &Q.Plan Plan = &Q.Plan
PlanSubscription = &Q.PlanSubscription
User = &Q.User User = &Q.User
UserPreference = &Q.UserPreference
Video = &Q.Video Video = &Q.Video
VideoAdConfig = &Q.VideoAdConfig
WalletTransaction = &Q.WalletTransaction
} }
func Use(db *gorm.DB, opts ...gen.DOOption) *Query { func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
return &Query{ return &Query{
db: db, db: db,
Payment: newPayment(db, opts...), AdTemplate: newAdTemplate(db, opts...),
Plan: newPlan(db, opts...), Domain: newDomain(db, opts...),
User: newUser(db, opts...), Job: newJob(db, opts...),
Video: newVideo(db, opts...), Notification: newNotification(db, opts...),
Payment: newPayment(db, opts...),
Plan: newPlan(db, opts...),
PlanSubscription: newPlanSubscription(db, opts...),
User: newUser(db, opts...),
UserPreference: newUserPreference(db, opts...),
Video: newVideo(db, opts...),
VideoAdConfig: newVideoAdConfig(db, opts...),
WalletTransaction: newWalletTransaction(db, opts...),
} }
} }
type Query struct { type Query struct {
db *gorm.DB db *gorm.DB
Payment payment AdTemplate adTemplate
Plan plan Domain domain
User user Job job
Video video Notification notification
Payment payment
Plan plan
PlanSubscription planSubscription
User user
UserPreference userPreference
Video video
VideoAdConfig videoAdConfig
WalletTransaction walletTransaction
} }
func (q *Query) Available() bool { return q.db != nil } func (q *Query) Available() bool { return q.db != nil }
func (q *Query) clone(db *gorm.DB) *Query { func (q *Query) clone(db *gorm.DB) *Query {
return &Query{ return &Query{
db: db, db: db,
Payment: q.Payment.clone(db), AdTemplate: q.AdTemplate.clone(db),
Plan: q.Plan.clone(db), Domain: q.Domain.clone(db),
User: q.User.clone(db), Job: q.Job.clone(db),
Video: q.Video.clone(db), Notification: q.Notification.clone(db),
Payment: q.Payment.clone(db),
Plan: q.Plan.clone(db),
PlanSubscription: q.PlanSubscription.clone(db),
User: q.User.clone(db),
UserPreference: q.UserPreference.clone(db),
Video: q.Video.clone(db),
VideoAdConfig: q.VideoAdConfig.clone(db),
WalletTransaction: q.WalletTransaction.clone(db),
} }
} }
@@ -72,27 +112,51 @@ func (q *Query) WriteDB() *Query {
func (q *Query) ReplaceDB(db *gorm.DB) *Query { func (q *Query) ReplaceDB(db *gorm.DB) *Query {
return &Query{ return &Query{
db: db, db: db,
Payment: q.Payment.replaceDB(db), AdTemplate: q.AdTemplate.replaceDB(db),
Plan: q.Plan.replaceDB(db), Domain: q.Domain.replaceDB(db),
User: q.User.replaceDB(db), Job: q.Job.replaceDB(db),
Video: q.Video.replaceDB(db), Notification: q.Notification.replaceDB(db),
Payment: q.Payment.replaceDB(db),
Plan: q.Plan.replaceDB(db),
PlanSubscription: q.PlanSubscription.replaceDB(db),
User: q.User.replaceDB(db),
UserPreference: q.UserPreference.replaceDB(db),
Video: q.Video.replaceDB(db),
VideoAdConfig: q.VideoAdConfig.replaceDB(db),
WalletTransaction: q.WalletTransaction.replaceDB(db),
} }
} }
type queryCtx struct { type queryCtx struct {
Payment IPaymentDo AdTemplate IAdTemplateDo
Plan IPlanDo Domain IDomainDo
User IUserDo Job IJobDo
Video IVideoDo Notification INotificationDo
Payment IPaymentDo
Plan IPlanDo
PlanSubscription IPlanSubscriptionDo
User IUserDo
UserPreference IUserPreferenceDo
Video IVideoDo
VideoAdConfig IVideoAdConfigDo
WalletTransaction IWalletTransactionDo
} }
func (q *Query) WithContext(ctx context.Context) *queryCtx { func (q *Query) WithContext(ctx context.Context) *queryCtx {
return &queryCtx{ return &queryCtx{
Payment: q.Payment.WithContext(ctx), AdTemplate: q.AdTemplate.WithContext(ctx),
Plan: q.Plan.WithContext(ctx), Domain: q.Domain.WithContext(ctx),
User: q.User.WithContext(ctx), Job: q.Job.WithContext(ctx),
Video: q.Video.WithContext(ctx), Notification: q.Notification.WithContext(ctx),
Payment: q.Payment.WithContext(ctx),
Plan: q.Plan.WithContext(ctx),
PlanSubscription: q.PlanSubscription.WithContext(ctx),
User: q.User.WithContext(ctx),
UserPreference: q.UserPreference.WithContext(ctx),
Video: q.Video.WithContext(ctx),
VideoAdConfig: q.VideoAdConfig.WithContext(ctx),
WalletTransaction: q.WalletTransaction.WithContext(ctx),
} }
} }

View File

@@ -0,0 +1,455 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newJob(db *gorm.DB, opts ...gen.DOOption) job {
_job := job{}
_job.jobDo.UseDB(db, opts...)
_job.jobDo.UseModel(&model.Job{})
tableName := _job.jobDo.TableName()
_job.ALL = field.NewAsterisk(tableName)
_job.ID = field.NewString(tableName, "id")
_job.Status = field.NewString(tableName, "status")
_job.Priority = field.NewInt64(tableName, "priority")
_job.InputURL = field.NewString(tableName, "input_url")
_job.OutputURL = field.NewString(tableName, "output_url")
_job.TotalDuration = field.NewInt64(tableName, "total_duration")
_job.CurrentTime = field.NewInt64(tableName, "current_time")
_job.Progress = field.NewFloat64(tableName, "progress")
_job.AgentID = field.NewInt64(tableName, "agent_id")
_job.Logs = field.NewString(tableName, "logs")
_job.Config = field.NewString(tableName, "config")
_job.Cancelled = field.NewBool(tableName, "cancelled")
_job.RetryCount = field.NewInt64(tableName, "retry_count")
_job.MaxRetries = field.NewInt64(tableName, "max_retries")
_job.CreatedAt = field.NewTime(tableName, "created_at")
_job.UpdatedAt = field.NewTime(tableName, "updated_at")
_job.Version = field.NewInt64(tableName, "version")
_job.fillFieldMap()
return _job
}
type job struct {
jobDo jobDo
ALL field.Asterisk
ID field.String
Status field.String
Priority field.Int64
InputURL field.String
OutputURL field.String
TotalDuration field.Int64
CurrentTime field.Int64
Progress field.Float64
AgentID field.Int64
Logs field.String
Config field.String
Cancelled field.Bool
RetryCount field.Int64
MaxRetries field.Int64
CreatedAt field.Time
UpdatedAt field.Time
Version field.Int64
fieldMap map[string]field.Expr
}
func (j job) Table(newTableName string) *job {
j.jobDo.UseTable(newTableName)
return j.updateTableName(newTableName)
}
func (j job) As(alias string) *job {
j.jobDo.DO = *(j.jobDo.As(alias).(*gen.DO))
return j.updateTableName(alias)
}
func (j *job) updateTableName(table string) *job {
j.ALL = field.NewAsterisk(table)
j.ID = field.NewString(table, "id")
j.Status = field.NewString(table, "status")
j.Priority = field.NewInt64(table, "priority")
j.InputURL = field.NewString(table, "input_url")
j.OutputURL = field.NewString(table, "output_url")
j.TotalDuration = field.NewInt64(table, "total_duration")
j.CurrentTime = field.NewInt64(table, "current_time")
j.Progress = field.NewFloat64(table, "progress")
j.AgentID = field.NewInt64(table, "agent_id")
j.Logs = field.NewString(table, "logs")
j.Config = field.NewString(table, "config")
j.Cancelled = field.NewBool(table, "cancelled")
j.RetryCount = field.NewInt64(table, "retry_count")
j.MaxRetries = field.NewInt64(table, "max_retries")
j.CreatedAt = field.NewTime(table, "created_at")
j.UpdatedAt = field.NewTime(table, "updated_at")
j.Version = field.NewInt64(table, "version")
j.fillFieldMap()
return j
}
func (j *job) WithContext(ctx context.Context) IJobDo { return j.jobDo.WithContext(ctx) }
func (j job) TableName() string { return j.jobDo.TableName() }
func (j job) Alias() string { return j.jobDo.Alias() }
func (j job) Columns(cols ...field.Expr) gen.Columns { return j.jobDo.Columns(cols...) }
func (j *job) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := j.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (j *job) fillFieldMap() {
j.fieldMap = make(map[string]field.Expr, 17)
j.fieldMap["id"] = j.ID
j.fieldMap["status"] = j.Status
j.fieldMap["priority"] = j.Priority
j.fieldMap["input_url"] = j.InputURL
j.fieldMap["output_url"] = j.OutputURL
j.fieldMap["total_duration"] = j.TotalDuration
j.fieldMap["current_time"] = j.CurrentTime
j.fieldMap["progress"] = j.Progress
j.fieldMap["agent_id"] = j.AgentID
j.fieldMap["logs"] = j.Logs
j.fieldMap["config"] = j.Config
j.fieldMap["cancelled"] = j.Cancelled
j.fieldMap["retry_count"] = j.RetryCount
j.fieldMap["max_retries"] = j.MaxRetries
j.fieldMap["created_at"] = j.CreatedAt
j.fieldMap["updated_at"] = j.UpdatedAt
j.fieldMap["version"] = j.Version
}
func (j job) clone(db *gorm.DB) job {
j.jobDo.ReplaceConnPool(db.Statement.ConnPool)
return j
}
func (j job) replaceDB(db *gorm.DB) job {
j.jobDo.ReplaceDB(db)
return j
}
type jobDo struct{ gen.DO }
type IJobDo interface {
gen.SubQuery
Debug() IJobDo
WithContext(ctx context.Context) IJobDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IJobDo
WriteDB() IJobDo
As(alias string) gen.Dao
Session(config *gorm.Session) IJobDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IJobDo
Not(conds ...gen.Condition) IJobDo
Or(conds ...gen.Condition) IJobDo
Select(conds ...field.Expr) IJobDo
Where(conds ...gen.Condition) IJobDo
Order(conds ...field.Expr) IJobDo
Distinct(cols ...field.Expr) IJobDo
Omit(cols ...field.Expr) IJobDo
Join(table schema.Tabler, on ...field.Expr) IJobDo
LeftJoin(table schema.Tabler, on ...field.Expr) IJobDo
RightJoin(table schema.Tabler, on ...field.Expr) IJobDo
Group(cols ...field.Expr) IJobDo
Having(conds ...gen.Condition) IJobDo
Limit(limit int) IJobDo
Offset(offset int) IJobDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IJobDo
Unscoped() IJobDo
Create(values ...*model.Job) error
CreateInBatches(values []*model.Job, batchSize int) error
Save(values ...*model.Job) error
First() (*model.Job, error)
Take() (*model.Job, error)
Last() (*model.Job, error)
Find() ([]*model.Job, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Job, err error)
FindInBatches(result *[]*model.Job, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.Job) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IJobDo
Assign(attrs ...field.AssignExpr) IJobDo
Joins(fields ...field.RelationField) IJobDo
Preload(fields ...field.RelationField) IJobDo
FirstOrInit() (*model.Job, error)
FirstOrCreate() (*model.Job, error)
FindByPage(offset int, limit int) (result []*model.Job, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IJobDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (j jobDo) Debug() IJobDo {
return j.withDO(j.DO.Debug())
}
func (j jobDo) WithContext(ctx context.Context) IJobDo {
return j.withDO(j.DO.WithContext(ctx))
}
func (j jobDo) ReadDB() IJobDo {
return j.Clauses(dbresolver.Read)
}
func (j jobDo) WriteDB() IJobDo {
return j.Clauses(dbresolver.Write)
}
func (j jobDo) Session(config *gorm.Session) IJobDo {
return j.withDO(j.DO.Session(config))
}
func (j jobDo) Clauses(conds ...clause.Expression) IJobDo {
return j.withDO(j.DO.Clauses(conds...))
}
func (j jobDo) Returning(value interface{}, columns ...string) IJobDo {
return j.withDO(j.DO.Returning(value, columns...))
}
func (j jobDo) Not(conds ...gen.Condition) IJobDo {
return j.withDO(j.DO.Not(conds...))
}
func (j jobDo) Or(conds ...gen.Condition) IJobDo {
return j.withDO(j.DO.Or(conds...))
}
func (j jobDo) Select(conds ...field.Expr) IJobDo {
return j.withDO(j.DO.Select(conds...))
}
func (j jobDo) Where(conds ...gen.Condition) IJobDo {
return j.withDO(j.DO.Where(conds...))
}
func (j jobDo) Order(conds ...field.Expr) IJobDo {
return j.withDO(j.DO.Order(conds...))
}
func (j jobDo) Distinct(cols ...field.Expr) IJobDo {
return j.withDO(j.DO.Distinct(cols...))
}
func (j jobDo) Omit(cols ...field.Expr) IJobDo {
return j.withDO(j.DO.Omit(cols...))
}
func (j jobDo) Join(table schema.Tabler, on ...field.Expr) IJobDo {
return j.withDO(j.DO.Join(table, on...))
}
func (j jobDo) LeftJoin(table schema.Tabler, on ...field.Expr) IJobDo {
return j.withDO(j.DO.LeftJoin(table, on...))
}
func (j jobDo) RightJoin(table schema.Tabler, on ...field.Expr) IJobDo {
return j.withDO(j.DO.RightJoin(table, on...))
}
func (j jobDo) Group(cols ...field.Expr) IJobDo {
return j.withDO(j.DO.Group(cols...))
}
func (j jobDo) Having(conds ...gen.Condition) IJobDo {
return j.withDO(j.DO.Having(conds...))
}
func (j jobDo) Limit(limit int) IJobDo {
return j.withDO(j.DO.Limit(limit))
}
func (j jobDo) Offset(offset int) IJobDo {
return j.withDO(j.DO.Offset(offset))
}
func (j jobDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IJobDo {
return j.withDO(j.DO.Scopes(funcs...))
}
func (j jobDo) Unscoped() IJobDo {
return j.withDO(j.DO.Unscoped())
}
func (j jobDo) Create(values ...*model.Job) error {
if len(values) == 0 {
return nil
}
return j.DO.Create(values)
}
func (j jobDo) CreateInBatches(values []*model.Job, batchSize int) error {
return j.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (j jobDo) Save(values ...*model.Job) error {
if len(values) == 0 {
return nil
}
return j.DO.Save(values)
}
func (j jobDo) First() (*model.Job, error) {
if result, err := j.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.Job), nil
}
}
func (j jobDo) Take() (*model.Job, error) {
if result, err := j.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.Job), nil
}
}
func (j jobDo) Last() (*model.Job, error) {
if result, err := j.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.Job), nil
}
}
func (j jobDo) Find() ([]*model.Job, error) {
result, err := j.DO.Find()
return result.([]*model.Job), err
}
func (j jobDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Job, err error) {
buf := make([]*model.Job, 0, batchSize)
err = j.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (j jobDo) FindInBatches(result *[]*model.Job, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return j.DO.FindInBatches(result, batchSize, fc)
}
func (j jobDo) Attrs(attrs ...field.AssignExpr) IJobDo {
return j.withDO(j.DO.Attrs(attrs...))
}
func (j jobDo) Assign(attrs ...field.AssignExpr) IJobDo {
return j.withDO(j.DO.Assign(attrs...))
}
func (j jobDo) Joins(fields ...field.RelationField) IJobDo {
for _, _f := range fields {
j = *j.withDO(j.DO.Joins(_f))
}
return &j
}
func (j jobDo) Preload(fields ...field.RelationField) IJobDo {
for _, _f := range fields {
j = *j.withDO(j.DO.Preload(_f))
}
return &j
}
func (j jobDo) FirstOrInit() (*model.Job, error) {
if result, err := j.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.Job), nil
}
}
func (j jobDo) FirstOrCreate() (*model.Job, error) {
if result, err := j.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.Job), nil
}
}
func (j jobDo) FindByPage(offset int, limit int) (result []*model.Job, count int64, err error) {
result, err = j.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = j.Offset(-1).Limit(-1).Count()
return
}
func (j jobDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = j.Count()
if err != nil {
return
}
err = j.Offset(offset).Limit(limit).Scan(result)
return
}
func (j jobDo) Scan(result interface{}) (err error) {
return j.DO.Scan(result)
}
func (j jobDo) Delete(models ...*model.Job) (result gen.ResultInfo, err error) {
return j.DO.Delete(models)
}
func (j *jobDo) withDO(do gen.Dao) *jobDo {
j.DO = *do.(*gen.DO)
return j
}

View File

@@ -0,0 +1,439 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newNotification(db *gorm.DB, opts ...gen.DOOption) notification {
_notification := notification{}
_notification.notificationDo.UseDB(db, opts...)
_notification.notificationDo.UseModel(&model.Notification{})
tableName := _notification.notificationDo.TableName()
_notification.ALL = field.NewAsterisk(tableName)
_notification.ID = field.NewString(tableName, "id")
_notification.UserID = field.NewString(tableName, "user_id")
_notification.Type = field.NewString(tableName, "type")
_notification.Title = field.NewString(tableName, "title")
_notification.Message = field.NewString(tableName, "message")
_notification.Metadata = field.NewString(tableName, "metadata")
_notification.ActionURL = field.NewString(tableName, "action_url")
_notification.ActionLabel = field.NewString(tableName, "action_label")
_notification.IsRead = field.NewBool(tableName, "is_read")
_notification.CreatedAt = field.NewTime(tableName, "created_at")
_notification.UpdatedAt = field.NewTime(tableName, "updated_at")
_notification.Version = field.NewInt64(tableName, "version")
_notification.fillFieldMap()
return _notification
}
type notification struct {
notificationDo notificationDo
ALL field.Asterisk
ID field.String
UserID field.String
Type field.String
Title field.String
Message field.String
Metadata field.String
ActionURL field.String
ActionLabel field.String
IsRead field.Bool
CreatedAt field.Time
UpdatedAt field.Time
Version field.Int64
fieldMap map[string]field.Expr
}
func (n notification) Table(newTableName string) *notification {
n.notificationDo.UseTable(newTableName)
return n.updateTableName(newTableName)
}
func (n notification) As(alias string) *notification {
n.notificationDo.DO = *(n.notificationDo.As(alias).(*gen.DO))
return n.updateTableName(alias)
}
func (n *notification) updateTableName(table string) *notification {
n.ALL = field.NewAsterisk(table)
n.ID = field.NewString(table, "id")
n.UserID = field.NewString(table, "user_id")
n.Type = field.NewString(table, "type")
n.Title = field.NewString(table, "title")
n.Message = field.NewString(table, "message")
n.Metadata = field.NewString(table, "metadata")
n.ActionURL = field.NewString(table, "action_url")
n.ActionLabel = field.NewString(table, "action_label")
n.IsRead = field.NewBool(table, "is_read")
n.CreatedAt = field.NewTime(table, "created_at")
n.UpdatedAt = field.NewTime(table, "updated_at")
n.Version = field.NewInt64(table, "version")
n.fillFieldMap()
return n
}
func (n *notification) WithContext(ctx context.Context) INotificationDo {
return n.notificationDo.WithContext(ctx)
}
func (n notification) TableName() string { return n.notificationDo.TableName() }
func (n notification) Alias() string { return n.notificationDo.Alias() }
func (n notification) Columns(cols ...field.Expr) gen.Columns {
return n.notificationDo.Columns(cols...)
}
func (n *notification) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := n.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (n *notification) fillFieldMap() {
n.fieldMap = make(map[string]field.Expr, 12)
n.fieldMap["id"] = n.ID
n.fieldMap["user_id"] = n.UserID
n.fieldMap["type"] = n.Type
n.fieldMap["title"] = n.Title
n.fieldMap["message"] = n.Message
n.fieldMap["metadata"] = n.Metadata
n.fieldMap["action_url"] = n.ActionURL
n.fieldMap["action_label"] = n.ActionLabel
n.fieldMap["is_read"] = n.IsRead
n.fieldMap["created_at"] = n.CreatedAt
n.fieldMap["updated_at"] = n.UpdatedAt
n.fieldMap["version"] = n.Version
}
func (n notification) clone(db *gorm.DB) notification {
n.notificationDo.ReplaceConnPool(db.Statement.ConnPool)
return n
}
func (n notification) replaceDB(db *gorm.DB) notification {
n.notificationDo.ReplaceDB(db)
return n
}
type notificationDo struct{ gen.DO }
type INotificationDo interface {
gen.SubQuery
Debug() INotificationDo
WithContext(ctx context.Context) INotificationDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() INotificationDo
WriteDB() INotificationDo
As(alias string) gen.Dao
Session(config *gorm.Session) INotificationDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) INotificationDo
Not(conds ...gen.Condition) INotificationDo
Or(conds ...gen.Condition) INotificationDo
Select(conds ...field.Expr) INotificationDo
Where(conds ...gen.Condition) INotificationDo
Order(conds ...field.Expr) INotificationDo
Distinct(cols ...field.Expr) INotificationDo
Omit(cols ...field.Expr) INotificationDo
Join(table schema.Tabler, on ...field.Expr) INotificationDo
LeftJoin(table schema.Tabler, on ...field.Expr) INotificationDo
RightJoin(table schema.Tabler, on ...field.Expr) INotificationDo
Group(cols ...field.Expr) INotificationDo
Having(conds ...gen.Condition) INotificationDo
Limit(limit int) INotificationDo
Offset(offset int) INotificationDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) INotificationDo
Unscoped() INotificationDo
Create(values ...*model.Notification) error
CreateInBatches(values []*model.Notification, batchSize int) error
Save(values ...*model.Notification) error
First() (*model.Notification, error)
Take() (*model.Notification, error)
Last() (*model.Notification, error)
Find() ([]*model.Notification, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Notification, err error)
FindInBatches(result *[]*model.Notification, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.Notification) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) INotificationDo
Assign(attrs ...field.AssignExpr) INotificationDo
Joins(fields ...field.RelationField) INotificationDo
Preload(fields ...field.RelationField) INotificationDo
FirstOrInit() (*model.Notification, error)
FirstOrCreate() (*model.Notification, error)
FindByPage(offset int, limit int) (result []*model.Notification, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) INotificationDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (n notificationDo) Debug() INotificationDo {
return n.withDO(n.DO.Debug())
}
func (n notificationDo) WithContext(ctx context.Context) INotificationDo {
return n.withDO(n.DO.WithContext(ctx))
}
func (n notificationDo) ReadDB() INotificationDo {
return n.Clauses(dbresolver.Read)
}
func (n notificationDo) WriteDB() INotificationDo {
return n.Clauses(dbresolver.Write)
}
func (n notificationDo) Session(config *gorm.Session) INotificationDo {
return n.withDO(n.DO.Session(config))
}
func (n notificationDo) Clauses(conds ...clause.Expression) INotificationDo {
return n.withDO(n.DO.Clauses(conds...))
}
func (n notificationDo) Returning(value interface{}, columns ...string) INotificationDo {
return n.withDO(n.DO.Returning(value, columns...))
}
func (n notificationDo) Not(conds ...gen.Condition) INotificationDo {
return n.withDO(n.DO.Not(conds...))
}
func (n notificationDo) Or(conds ...gen.Condition) INotificationDo {
return n.withDO(n.DO.Or(conds...))
}
func (n notificationDo) Select(conds ...field.Expr) INotificationDo {
return n.withDO(n.DO.Select(conds...))
}
func (n notificationDo) Where(conds ...gen.Condition) INotificationDo {
return n.withDO(n.DO.Where(conds...))
}
func (n notificationDo) Order(conds ...field.Expr) INotificationDo {
return n.withDO(n.DO.Order(conds...))
}
func (n notificationDo) Distinct(cols ...field.Expr) INotificationDo {
return n.withDO(n.DO.Distinct(cols...))
}
func (n notificationDo) Omit(cols ...field.Expr) INotificationDo {
return n.withDO(n.DO.Omit(cols...))
}
func (n notificationDo) Join(table schema.Tabler, on ...field.Expr) INotificationDo {
return n.withDO(n.DO.Join(table, on...))
}
func (n notificationDo) LeftJoin(table schema.Tabler, on ...field.Expr) INotificationDo {
return n.withDO(n.DO.LeftJoin(table, on...))
}
func (n notificationDo) RightJoin(table schema.Tabler, on ...field.Expr) INotificationDo {
return n.withDO(n.DO.RightJoin(table, on...))
}
func (n notificationDo) Group(cols ...field.Expr) INotificationDo {
return n.withDO(n.DO.Group(cols...))
}
func (n notificationDo) Having(conds ...gen.Condition) INotificationDo {
return n.withDO(n.DO.Having(conds...))
}
func (n notificationDo) Limit(limit int) INotificationDo {
return n.withDO(n.DO.Limit(limit))
}
func (n notificationDo) Offset(offset int) INotificationDo {
return n.withDO(n.DO.Offset(offset))
}
func (n notificationDo) Scopes(funcs ...func(gen.Dao) gen.Dao) INotificationDo {
return n.withDO(n.DO.Scopes(funcs...))
}
func (n notificationDo) Unscoped() INotificationDo {
return n.withDO(n.DO.Unscoped())
}
func (n notificationDo) Create(values ...*model.Notification) error {
if len(values) == 0 {
return nil
}
return n.DO.Create(values)
}
func (n notificationDo) CreateInBatches(values []*model.Notification, batchSize int) error {
return n.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (n notificationDo) Save(values ...*model.Notification) error {
if len(values) == 0 {
return nil
}
return n.DO.Save(values)
}
func (n notificationDo) First() (*model.Notification, error) {
if result, err := n.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.Notification), nil
}
}
func (n notificationDo) Take() (*model.Notification, error) {
if result, err := n.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.Notification), nil
}
}
func (n notificationDo) Last() (*model.Notification, error) {
if result, err := n.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.Notification), nil
}
}
func (n notificationDo) Find() ([]*model.Notification, error) {
result, err := n.DO.Find()
return result.([]*model.Notification), err
}
func (n notificationDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Notification, err error) {
buf := make([]*model.Notification, 0, batchSize)
err = n.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (n notificationDo) FindInBatches(result *[]*model.Notification, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return n.DO.FindInBatches(result, batchSize, fc)
}
func (n notificationDo) Attrs(attrs ...field.AssignExpr) INotificationDo {
return n.withDO(n.DO.Attrs(attrs...))
}
func (n notificationDo) Assign(attrs ...field.AssignExpr) INotificationDo {
return n.withDO(n.DO.Assign(attrs...))
}
func (n notificationDo) Joins(fields ...field.RelationField) INotificationDo {
for _, _f := range fields {
n = *n.withDO(n.DO.Joins(_f))
}
return &n
}
func (n notificationDo) Preload(fields ...field.RelationField) INotificationDo {
for _, _f := range fields {
n = *n.withDO(n.DO.Preload(_f))
}
return &n
}
func (n notificationDo) FirstOrInit() (*model.Notification, error) {
if result, err := n.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.Notification), nil
}
}
func (n notificationDo) FirstOrCreate() (*model.Notification, error) {
if result, err := n.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.Notification), nil
}
}
func (n notificationDo) FindByPage(offset int, limit int) (result []*model.Notification, count int64, err error) {
result, err = n.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = n.Offset(-1).Limit(-1).Count()
return
}
func (n notificationDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = n.Count()
if err != nil {
return
}
err = n.Offset(offset).Limit(limit).Scan(result)
return
}
func (n notificationDo) Scan(result interface{}) (err error) {
return n.DO.Scan(result)
}
func (n notificationDo) Delete(models ...*model.Notification) (result gen.ResultInfo, err error) {
return n.DO.Delete(models)
}
func (n *notificationDo) withDO(do gen.Dao) *notificationDo {
n.DO = *do.(*gen.DO)
return n
}

View File

@@ -37,7 +37,7 @@ func newPlan(db *gorm.DB, opts ...gen.DOOption) plan {
_plan.UploadLimit = field.NewInt32(tableName, "upload_limit") _plan.UploadLimit = field.NewInt32(tableName, "upload_limit")
_plan.DurationLimit = field.NewInt32(tableName, "duration_limit") _plan.DurationLimit = field.NewInt32(tableName, "duration_limit")
_plan.QualityLimit = field.NewString(tableName, "quality_limit") _plan.QualityLimit = field.NewString(tableName, "quality_limit")
_plan.Features = field.NewString(tableName, "features") _plan.Features = field.NewField(tableName, "features")
_plan.IsActive = field.NewBool(tableName, "is_active") _plan.IsActive = field.NewBool(tableName, "is_active")
_plan.Version = field.NewInt64(tableName, "version") _plan.Version = field.NewInt64(tableName, "version")
@@ -59,7 +59,7 @@ type plan struct {
UploadLimit field.Int32 UploadLimit field.Int32
DurationLimit field.Int32 DurationLimit field.Int32
QualityLimit field.String QualityLimit field.String
Features field.String Features field.Field
IsActive field.Bool IsActive field.Bool
Version field.Int64 Version field.Int64
@@ -87,7 +87,7 @@ func (p *plan) updateTableName(table string) *plan {
p.UploadLimit = field.NewInt32(table, "upload_limit") p.UploadLimit = field.NewInt32(table, "upload_limit")
p.DurationLimit = field.NewInt32(table, "duration_limit") p.DurationLimit = field.NewInt32(table, "duration_limit")
p.QualityLimit = field.NewString(table, "quality_limit") p.QualityLimit = field.NewString(table, "quality_limit")
p.Features = field.NewString(table, "features") p.Features = field.NewField(table, "features")
p.IsActive = field.NewBool(table, "is_active") p.IsActive = field.NewBool(table, "is_active")
p.Version = field.NewInt64(table, "version") p.Version = field.NewInt64(table, "version")

View File

@@ -0,0 +1,455 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newPlanSubscription(db *gorm.DB, opts ...gen.DOOption) planSubscription {
_planSubscription := planSubscription{}
_planSubscription.planSubscriptionDo.UseDB(db, opts...)
_planSubscription.planSubscriptionDo.UseModel(&model.PlanSubscription{})
tableName := _planSubscription.planSubscriptionDo.TableName()
_planSubscription.ALL = field.NewAsterisk(tableName)
_planSubscription.ID = field.NewString(tableName, "id")
_planSubscription.UserID = field.NewString(tableName, "user_id")
_planSubscription.PaymentID = field.NewString(tableName, "payment_id")
_planSubscription.PlanID = field.NewString(tableName, "plan_id")
_planSubscription.TermMonths = field.NewInt32(tableName, "term_months")
_planSubscription.PaymentMethod = field.NewString(tableName, "payment_method")
_planSubscription.WalletAmount = field.NewFloat64(tableName, "wallet_amount")
_planSubscription.TopupAmount = field.NewFloat64(tableName, "topup_amount")
_planSubscription.StartedAt = field.NewTime(tableName, "started_at")
_planSubscription.ExpiresAt = field.NewTime(tableName, "expires_at")
_planSubscription.Reminder7DSentAt = field.NewTime(tableName, "reminder_7d_sent_at")
_planSubscription.Reminder3DSentAt = field.NewTime(tableName, "reminder_3d_sent_at")
_planSubscription.Reminder1DSentAt = field.NewTime(tableName, "reminder_1d_sent_at")
_planSubscription.CreatedAt = field.NewTime(tableName, "created_at")
_planSubscription.UpdatedAt = field.NewTime(tableName, "updated_at")
_planSubscription.Version = field.NewInt64(tableName, "version")
_planSubscription.fillFieldMap()
return _planSubscription
}
type planSubscription struct {
planSubscriptionDo planSubscriptionDo
ALL field.Asterisk
ID field.String
UserID field.String
PaymentID field.String
PlanID field.String
TermMonths field.Int32
PaymentMethod field.String
WalletAmount field.Float64
TopupAmount field.Float64
StartedAt field.Time
ExpiresAt field.Time
Reminder7DSentAt field.Time
Reminder3DSentAt field.Time
Reminder1DSentAt field.Time
CreatedAt field.Time
UpdatedAt field.Time
Version field.Int64
fieldMap map[string]field.Expr
}
func (p planSubscription) Table(newTableName string) *planSubscription {
p.planSubscriptionDo.UseTable(newTableName)
return p.updateTableName(newTableName)
}
func (p planSubscription) As(alias string) *planSubscription {
p.planSubscriptionDo.DO = *(p.planSubscriptionDo.As(alias).(*gen.DO))
return p.updateTableName(alias)
}
func (p *planSubscription) updateTableName(table string) *planSubscription {
p.ALL = field.NewAsterisk(table)
p.ID = field.NewString(table, "id")
p.UserID = field.NewString(table, "user_id")
p.PaymentID = field.NewString(table, "payment_id")
p.PlanID = field.NewString(table, "plan_id")
p.TermMonths = field.NewInt32(table, "term_months")
p.PaymentMethod = field.NewString(table, "payment_method")
p.WalletAmount = field.NewFloat64(table, "wallet_amount")
p.TopupAmount = field.NewFloat64(table, "topup_amount")
p.StartedAt = field.NewTime(table, "started_at")
p.ExpiresAt = field.NewTime(table, "expires_at")
p.Reminder7DSentAt = field.NewTime(table, "reminder_7d_sent_at")
p.Reminder3DSentAt = field.NewTime(table, "reminder_3d_sent_at")
p.Reminder1DSentAt = field.NewTime(table, "reminder_1d_sent_at")
p.CreatedAt = field.NewTime(table, "created_at")
p.UpdatedAt = field.NewTime(table, "updated_at")
p.Version = field.NewInt64(table, "version")
p.fillFieldMap()
return p
}
func (p *planSubscription) WithContext(ctx context.Context) IPlanSubscriptionDo {
return p.planSubscriptionDo.WithContext(ctx)
}
func (p planSubscription) TableName() string { return p.planSubscriptionDo.TableName() }
func (p planSubscription) Alias() string { return p.planSubscriptionDo.Alias() }
func (p planSubscription) Columns(cols ...field.Expr) gen.Columns {
return p.planSubscriptionDo.Columns(cols...)
}
func (p *planSubscription) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := p.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (p *planSubscription) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 16)
p.fieldMap["id"] = p.ID
p.fieldMap["user_id"] = p.UserID
p.fieldMap["payment_id"] = p.PaymentID
p.fieldMap["plan_id"] = p.PlanID
p.fieldMap["term_months"] = p.TermMonths
p.fieldMap["payment_method"] = p.PaymentMethod
p.fieldMap["wallet_amount"] = p.WalletAmount
p.fieldMap["topup_amount"] = p.TopupAmount
p.fieldMap["started_at"] = p.StartedAt
p.fieldMap["expires_at"] = p.ExpiresAt
p.fieldMap["reminder_7d_sent_at"] = p.Reminder7DSentAt
p.fieldMap["reminder_3d_sent_at"] = p.Reminder3DSentAt
p.fieldMap["reminder_1d_sent_at"] = p.Reminder1DSentAt
p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt
p.fieldMap["version"] = p.Version
}
func (p planSubscription) clone(db *gorm.DB) planSubscription {
p.planSubscriptionDo.ReplaceConnPool(db.Statement.ConnPool)
return p
}
func (p planSubscription) replaceDB(db *gorm.DB) planSubscription {
p.planSubscriptionDo.ReplaceDB(db)
return p
}
type planSubscriptionDo struct{ gen.DO }
type IPlanSubscriptionDo interface {
gen.SubQuery
Debug() IPlanSubscriptionDo
WithContext(ctx context.Context) IPlanSubscriptionDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IPlanSubscriptionDo
WriteDB() IPlanSubscriptionDo
As(alias string) gen.Dao
Session(config *gorm.Session) IPlanSubscriptionDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IPlanSubscriptionDo
Not(conds ...gen.Condition) IPlanSubscriptionDo
Or(conds ...gen.Condition) IPlanSubscriptionDo
Select(conds ...field.Expr) IPlanSubscriptionDo
Where(conds ...gen.Condition) IPlanSubscriptionDo
Order(conds ...field.Expr) IPlanSubscriptionDo
Distinct(cols ...field.Expr) IPlanSubscriptionDo
Omit(cols ...field.Expr) IPlanSubscriptionDo
Join(table schema.Tabler, on ...field.Expr) IPlanSubscriptionDo
LeftJoin(table schema.Tabler, on ...field.Expr) IPlanSubscriptionDo
RightJoin(table schema.Tabler, on ...field.Expr) IPlanSubscriptionDo
Group(cols ...field.Expr) IPlanSubscriptionDo
Having(conds ...gen.Condition) IPlanSubscriptionDo
Limit(limit int) IPlanSubscriptionDo
Offset(offset int) IPlanSubscriptionDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IPlanSubscriptionDo
Unscoped() IPlanSubscriptionDo
Create(values ...*model.PlanSubscription) error
CreateInBatches(values []*model.PlanSubscription, batchSize int) error
Save(values ...*model.PlanSubscription) error
First() (*model.PlanSubscription, error)
Take() (*model.PlanSubscription, error)
Last() (*model.PlanSubscription, error)
Find() ([]*model.PlanSubscription, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.PlanSubscription, err error)
FindInBatches(result *[]*model.PlanSubscription, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.PlanSubscription) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IPlanSubscriptionDo
Assign(attrs ...field.AssignExpr) IPlanSubscriptionDo
Joins(fields ...field.RelationField) IPlanSubscriptionDo
Preload(fields ...field.RelationField) IPlanSubscriptionDo
FirstOrInit() (*model.PlanSubscription, error)
FirstOrCreate() (*model.PlanSubscription, error)
FindByPage(offset int, limit int) (result []*model.PlanSubscription, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IPlanSubscriptionDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (p planSubscriptionDo) Debug() IPlanSubscriptionDo {
return p.withDO(p.DO.Debug())
}
func (p planSubscriptionDo) WithContext(ctx context.Context) IPlanSubscriptionDo {
return p.withDO(p.DO.WithContext(ctx))
}
func (p planSubscriptionDo) ReadDB() IPlanSubscriptionDo {
return p.Clauses(dbresolver.Read)
}
func (p planSubscriptionDo) WriteDB() IPlanSubscriptionDo {
return p.Clauses(dbresolver.Write)
}
func (p planSubscriptionDo) Session(config *gorm.Session) IPlanSubscriptionDo {
return p.withDO(p.DO.Session(config))
}
func (p planSubscriptionDo) Clauses(conds ...clause.Expression) IPlanSubscriptionDo {
return p.withDO(p.DO.Clauses(conds...))
}
func (p planSubscriptionDo) Returning(value interface{}, columns ...string) IPlanSubscriptionDo {
return p.withDO(p.DO.Returning(value, columns...))
}
func (p planSubscriptionDo) Not(conds ...gen.Condition) IPlanSubscriptionDo {
return p.withDO(p.DO.Not(conds...))
}
func (p planSubscriptionDo) Or(conds ...gen.Condition) IPlanSubscriptionDo {
return p.withDO(p.DO.Or(conds...))
}
func (p planSubscriptionDo) Select(conds ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.Select(conds...))
}
func (p planSubscriptionDo) Where(conds ...gen.Condition) IPlanSubscriptionDo {
return p.withDO(p.DO.Where(conds...))
}
func (p planSubscriptionDo) Order(conds ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.Order(conds...))
}
func (p planSubscriptionDo) Distinct(cols ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.Distinct(cols...))
}
func (p planSubscriptionDo) Omit(cols ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.Omit(cols...))
}
func (p planSubscriptionDo) Join(table schema.Tabler, on ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.Join(table, on...))
}
func (p planSubscriptionDo) LeftJoin(table schema.Tabler, on ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.LeftJoin(table, on...))
}
func (p planSubscriptionDo) RightJoin(table schema.Tabler, on ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.RightJoin(table, on...))
}
func (p planSubscriptionDo) Group(cols ...field.Expr) IPlanSubscriptionDo {
return p.withDO(p.DO.Group(cols...))
}
func (p planSubscriptionDo) Having(conds ...gen.Condition) IPlanSubscriptionDo {
return p.withDO(p.DO.Having(conds...))
}
func (p planSubscriptionDo) Limit(limit int) IPlanSubscriptionDo {
return p.withDO(p.DO.Limit(limit))
}
func (p planSubscriptionDo) Offset(offset int) IPlanSubscriptionDo {
return p.withDO(p.DO.Offset(offset))
}
func (p planSubscriptionDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IPlanSubscriptionDo {
return p.withDO(p.DO.Scopes(funcs...))
}
func (p planSubscriptionDo) Unscoped() IPlanSubscriptionDo {
return p.withDO(p.DO.Unscoped())
}
func (p planSubscriptionDo) Create(values ...*model.PlanSubscription) error {
if len(values) == 0 {
return nil
}
return p.DO.Create(values)
}
func (p planSubscriptionDo) CreateInBatches(values []*model.PlanSubscription, batchSize int) error {
return p.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (p planSubscriptionDo) Save(values ...*model.PlanSubscription) error {
if len(values) == 0 {
return nil
}
return p.DO.Save(values)
}
func (p planSubscriptionDo) First() (*model.PlanSubscription, error) {
if result, err := p.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.PlanSubscription), nil
}
}
func (p planSubscriptionDo) Take() (*model.PlanSubscription, error) {
if result, err := p.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.PlanSubscription), nil
}
}
func (p planSubscriptionDo) Last() (*model.PlanSubscription, error) {
if result, err := p.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.PlanSubscription), nil
}
}
func (p planSubscriptionDo) Find() ([]*model.PlanSubscription, error) {
result, err := p.DO.Find()
return result.([]*model.PlanSubscription), err
}
func (p planSubscriptionDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.PlanSubscription, err error) {
buf := make([]*model.PlanSubscription, 0, batchSize)
err = p.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (p planSubscriptionDo) FindInBatches(result *[]*model.PlanSubscription, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return p.DO.FindInBatches(result, batchSize, fc)
}
func (p planSubscriptionDo) Attrs(attrs ...field.AssignExpr) IPlanSubscriptionDo {
return p.withDO(p.DO.Attrs(attrs...))
}
func (p planSubscriptionDo) Assign(attrs ...field.AssignExpr) IPlanSubscriptionDo {
return p.withDO(p.DO.Assign(attrs...))
}
func (p planSubscriptionDo) Joins(fields ...field.RelationField) IPlanSubscriptionDo {
for _, _f := range fields {
p = *p.withDO(p.DO.Joins(_f))
}
return &p
}
func (p planSubscriptionDo) Preload(fields ...field.RelationField) IPlanSubscriptionDo {
for _, _f := range fields {
p = *p.withDO(p.DO.Preload(_f))
}
return &p
}
func (p planSubscriptionDo) FirstOrInit() (*model.PlanSubscription, error) {
if result, err := p.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.PlanSubscription), nil
}
}
func (p planSubscriptionDo) FirstOrCreate() (*model.PlanSubscription, error) {
if result, err := p.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.PlanSubscription), nil
}
}
func (p planSubscriptionDo) FindByPage(offset int, limit int) (result []*model.PlanSubscription, count int64, err error) {
result, err = p.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = p.Offset(-1).Limit(-1).Count()
return
}
func (p planSubscriptionDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = p.Count()
if err != nil {
return
}
err = p.Offset(offset).Limit(limit).Scan(result)
return
}
func (p planSubscriptionDo) Scan(result interface{}) (err error) {
return p.DO.Scan(result)
}
func (p planSubscriptionDo) Delete(models ...*model.PlanSubscription) (result gen.ResultInfo, err error) {
return p.DO.Delete(models)
}
func (p *planSubscriptionDo) withDO(do gen.Dao) *planSubscriptionDo {
p.DO = *do.(*gen.DO)
return p
}

View File

@@ -0,0 +1,463 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newUserPreference(db *gorm.DB, opts ...gen.DOOption) userPreference {
_userPreference := userPreference{}
_userPreference.userPreferenceDo.UseDB(db, opts...)
_userPreference.userPreferenceDo.UseModel(&model.UserPreference{})
tableName := _userPreference.userPreferenceDo.TableName()
_userPreference.ALL = field.NewAsterisk(tableName)
_userPreference.UserID = field.NewString(tableName, "user_id")
_userPreference.Language = field.NewString(tableName, "language")
_userPreference.Locale = field.NewString(tableName, "locale")
_userPreference.EmailNotifications = field.NewBool(tableName, "email_notifications")
_userPreference.PushNotifications = field.NewBool(tableName, "push_notifications")
_userPreference.MarketingNotifications = field.NewBool(tableName, "marketing_notifications")
_userPreference.TelegramNotifications = field.NewBool(tableName, "telegram_notifications")
_userPreference.Autoplay = field.NewBool(tableName, "autoplay")
_userPreference.Loop = field.NewBool(tableName, "loop")
_userPreference.Muted = field.NewBool(tableName, "muted")
_userPreference.ShowControls = field.NewBool(tableName, "show_controls")
_userPreference.Pip = field.NewBool(tableName, "pip")
_userPreference.Airplay = field.NewBool(tableName, "airplay")
_userPreference.Chromecast = field.NewBool(tableName, "chromecast")
_userPreference.CreatedAt = field.NewTime(tableName, "created_at")
_userPreference.UpdatedAt = field.NewTime(tableName, "updated_at")
_userPreference.EncrytionM3u8 = field.NewBool(tableName, "encrytion_m3u8")
_userPreference.Version = field.NewInt64(tableName, "version")
_userPreference.fillFieldMap()
return _userPreference
}
type userPreference struct {
userPreferenceDo userPreferenceDo
ALL field.Asterisk
UserID field.String
Language field.String
Locale field.String
EmailNotifications field.Bool
PushNotifications field.Bool
MarketingNotifications field.Bool
TelegramNotifications field.Bool
Autoplay field.Bool
Loop field.Bool
Muted field.Bool
ShowControls field.Bool
Pip field.Bool
Airplay field.Bool
Chromecast field.Bool
CreatedAt field.Time
UpdatedAt field.Time
EncrytionM3u8 field.Bool
Version field.Int64
fieldMap map[string]field.Expr
}
func (u userPreference) Table(newTableName string) *userPreference {
u.userPreferenceDo.UseTable(newTableName)
return u.updateTableName(newTableName)
}
func (u userPreference) As(alias string) *userPreference {
u.userPreferenceDo.DO = *(u.userPreferenceDo.As(alias).(*gen.DO))
return u.updateTableName(alias)
}
func (u *userPreference) updateTableName(table string) *userPreference {
u.ALL = field.NewAsterisk(table)
u.UserID = field.NewString(table, "user_id")
u.Language = field.NewString(table, "language")
u.Locale = field.NewString(table, "locale")
u.EmailNotifications = field.NewBool(table, "email_notifications")
u.PushNotifications = field.NewBool(table, "push_notifications")
u.MarketingNotifications = field.NewBool(table, "marketing_notifications")
u.TelegramNotifications = field.NewBool(table, "telegram_notifications")
u.Autoplay = field.NewBool(table, "autoplay")
u.Loop = field.NewBool(table, "loop")
u.Muted = field.NewBool(table, "muted")
u.ShowControls = field.NewBool(table, "show_controls")
u.Pip = field.NewBool(table, "pip")
u.Airplay = field.NewBool(table, "airplay")
u.Chromecast = field.NewBool(table, "chromecast")
u.CreatedAt = field.NewTime(table, "created_at")
u.UpdatedAt = field.NewTime(table, "updated_at")
u.EncrytionM3u8 = field.NewBool(table, "encrytion_m3u8")
u.Version = field.NewInt64(table, "version")
u.fillFieldMap()
return u
}
func (u *userPreference) WithContext(ctx context.Context) IUserPreferenceDo {
return u.userPreferenceDo.WithContext(ctx)
}
func (u userPreference) TableName() string { return u.userPreferenceDo.TableName() }
func (u userPreference) Alias() string { return u.userPreferenceDo.Alias() }
func (u userPreference) Columns(cols ...field.Expr) gen.Columns {
return u.userPreferenceDo.Columns(cols...)
}
func (u *userPreference) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := u.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (u *userPreference) fillFieldMap() {
u.fieldMap = make(map[string]field.Expr, 18)
u.fieldMap["user_id"] = u.UserID
u.fieldMap["language"] = u.Language
u.fieldMap["locale"] = u.Locale
u.fieldMap["email_notifications"] = u.EmailNotifications
u.fieldMap["push_notifications"] = u.PushNotifications
u.fieldMap["marketing_notifications"] = u.MarketingNotifications
u.fieldMap["telegram_notifications"] = u.TelegramNotifications
u.fieldMap["autoplay"] = u.Autoplay
u.fieldMap["loop"] = u.Loop
u.fieldMap["muted"] = u.Muted
u.fieldMap["show_controls"] = u.ShowControls
u.fieldMap["pip"] = u.Pip
u.fieldMap["airplay"] = u.Airplay
u.fieldMap["chromecast"] = u.Chromecast
u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt
u.fieldMap["encrytion_m3u8"] = u.EncrytionM3u8
u.fieldMap["version"] = u.Version
}
func (u userPreference) clone(db *gorm.DB) userPreference {
u.userPreferenceDo.ReplaceConnPool(db.Statement.ConnPool)
return u
}
func (u userPreference) replaceDB(db *gorm.DB) userPreference {
u.userPreferenceDo.ReplaceDB(db)
return u
}
type userPreferenceDo struct{ gen.DO }
type IUserPreferenceDo interface {
gen.SubQuery
Debug() IUserPreferenceDo
WithContext(ctx context.Context) IUserPreferenceDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IUserPreferenceDo
WriteDB() IUserPreferenceDo
As(alias string) gen.Dao
Session(config *gorm.Session) IUserPreferenceDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IUserPreferenceDo
Not(conds ...gen.Condition) IUserPreferenceDo
Or(conds ...gen.Condition) IUserPreferenceDo
Select(conds ...field.Expr) IUserPreferenceDo
Where(conds ...gen.Condition) IUserPreferenceDo
Order(conds ...field.Expr) IUserPreferenceDo
Distinct(cols ...field.Expr) IUserPreferenceDo
Omit(cols ...field.Expr) IUserPreferenceDo
Join(table schema.Tabler, on ...field.Expr) IUserPreferenceDo
LeftJoin(table schema.Tabler, on ...field.Expr) IUserPreferenceDo
RightJoin(table schema.Tabler, on ...field.Expr) IUserPreferenceDo
Group(cols ...field.Expr) IUserPreferenceDo
Having(conds ...gen.Condition) IUserPreferenceDo
Limit(limit int) IUserPreferenceDo
Offset(offset int) IUserPreferenceDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IUserPreferenceDo
Unscoped() IUserPreferenceDo
Create(values ...*model.UserPreference) error
CreateInBatches(values []*model.UserPreference, batchSize int) error
Save(values ...*model.UserPreference) error
First() (*model.UserPreference, error)
Take() (*model.UserPreference, error)
Last() (*model.UserPreference, error)
Find() ([]*model.UserPreference, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UserPreference, err error)
FindInBatches(result *[]*model.UserPreference, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.UserPreference) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IUserPreferenceDo
Assign(attrs ...field.AssignExpr) IUserPreferenceDo
Joins(fields ...field.RelationField) IUserPreferenceDo
Preload(fields ...field.RelationField) IUserPreferenceDo
FirstOrInit() (*model.UserPreference, error)
FirstOrCreate() (*model.UserPreference, error)
FindByPage(offset int, limit int) (result []*model.UserPreference, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IUserPreferenceDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (u userPreferenceDo) Debug() IUserPreferenceDo {
return u.withDO(u.DO.Debug())
}
func (u userPreferenceDo) WithContext(ctx context.Context) IUserPreferenceDo {
return u.withDO(u.DO.WithContext(ctx))
}
func (u userPreferenceDo) ReadDB() IUserPreferenceDo {
return u.Clauses(dbresolver.Read)
}
func (u userPreferenceDo) WriteDB() IUserPreferenceDo {
return u.Clauses(dbresolver.Write)
}
func (u userPreferenceDo) Session(config *gorm.Session) IUserPreferenceDo {
return u.withDO(u.DO.Session(config))
}
func (u userPreferenceDo) Clauses(conds ...clause.Expression) IUserPreferenceDo {
return u.withDO(u.DO.Clauses(conds...))
}
func (u userPreferenceDo) Returning(value interface{}, columns ...string) IUserPreferenceDo {
return u.withDO(u.DO.Returning(value, columns...))
}
func (u userPreferenceDo) Not(conds ...gen.Condition) IUserPreferenceDo {
return u.withDO(u.DO.Not(conds...))
}
func (u userPreferenceDo) Or(conds ...gen.Condition) IUserPreferenceDo {
return u.withDO(u.DO.Or(conds...))
}
func (u userPreferenceDo) Select(conds ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.Select(conds...))
}
func (u userPreferenceDo) Where(conds ...gen.Condition) IUserPreferenceDo {
return u.withDO(u.DO.Where(conds...))
}
func (u userPreferenceDo) Order(conds ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.Order(conds...))
}
func (u userPreferenceDo) Distinct(cols ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.Distinct(cols...))
}
func (u userPreferenceDo) Omit(cols ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.Omit(cols...))
}
func (u userPreferenceDo) Join(table schema.Tabler, on ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.Join(table, on...))
}
func (u userPreferenceDo) LeftJoin(table schema.Tabler, on ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.LeftJoin(table, on...))
}
func (u userPreferenceDo) RightJoin(table schema.Tabler, on ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.RightJoin(table, on...))
}
func (u userPreferenceDo) Group(cols ...field.Expr) IUserPreferenceDo {
return u.withDO(u.DO.Group(cols...))
}
func (u userPreferenceDo) Having(conds ...gen.Condition) IUserPreferenceDo {
return u.withDO(u.DO.Having(conds...))
}
func (u userPreferenceDo) Limit(limit int) IUserPreferenceDo {
return u.withDO(u.DO.Limit(limit))
}
func (u userPreferenceDo) Offset(offset int) IUserPreferenceDo {
return u.withDO(u.DO.Offset(offset))
}
func (u userPreferenceDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IUserPreferenceDo {
return u.withDO(u.DO.Scopes(funcs...))
}
func (u userPreferenceDo) Unscoped() IUserPreferenceDo {
return u.withDO(u.DO.Unscoped())
}
func (u userPreferenceDo) Create(values ...*model.UserPreference) error {
if len(values) == 0 {
return nil
}
return u.DO.Create(values)
}
func (u userPreferenceDo) CreateInBatches(values []*model.UserPreference, batchSize int) error {
return u.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (u userPreferenceDo) Save(values ...*model.UserPreference) error {
if len(values) == 0 {
return nil
}
return u.DO.Save(values)
}
func (u userPreferenceDo) First() (*model.UserPreference, error) {
if result, err := u.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.UserPreference), nil
}
}
func (u userPreferenceDo) Take() (*model.UserPreference, error) {
if result, err := u.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.UserPreference), nil
}
}
func (u userPreferenceDo) Last() (*model.UserPreference, error) {
if result, err := u.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.UserPreference), nil
}
}
func (u userPreferenceDo) Find() ([]*model.UserPreference, error) {
result, err := u.DO.Find()
return result.([]*model.UserPreference), err
}
func (u userPreferenceDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UserPreference, err error) {
buf := make([]*model.UserPreference, 0, batchSize)
err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (u userPreferenceDo) FindInBatches(result *[]*model.UserPreference, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return u.DO.FindInBatches(result, batchSize, fc)
}
func (u userPreferenceDo) Attrs(attrs ...field.AssignExpr) IUserPreferenceDo {
return u.withDO(u.DO.Attrs(attrs...))
}
func (u userPreferenceDo) Assign(attrs ...field.AssignExpr) IUserPreferenceDo {
return u.withDO(u.DO.Assign(attrs...))
}
func (u userPreferenceDo) Joins(fields ...field.RelationField) IUserPreferenceDo {
for _, _f := range fields {
u = *u.withDO(u.DO.Joins(_f))
}
return &u
}
func (u userPreferenceDo) Preload(fields ...field.RelationField) IUserPreferenceDo {
for _, _f := range fields {
u = *u.withDO(u.DO.Preload(_f))
}
return &u
}
func (u userPreferenceDo) FirstOrInit() (*model.UserPreference, error) {
if result, err := u.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.UserPreference), nil
}
}
func (u userPreferenceDo) FirstOrCreate() (*model.UserPreference, error) {
if result, err := u.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.UserPreference), nil
}
}
func (u userPreferenceDo) FindByPage(offset int, limit int) (result []*model.UserPreference, count int64, err error) {
result, err = u.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = u.Offset(-1).Limit(-1).Count()
return
}
func (u userPreferenceDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = u.Count()
if err != nil {
return
}
err = u.Offset(offset).Limit(limit).Scan(result)
return
}
func (u userPreferenceDo) Scan(result interface{}) (err error) {
return u.DO.Scan(result)
}
func (u userPreferenceDo) Delete(models ...*model.UserPreference) (result gen.ResultInfo, err error) {
return u.DO.Delete(models)
}
func (u *userPreferenceDo) withDO(do gen.Dao) *userPreferenceDo {
u.DO = *do.(*gen.DO)
return u
}

View File

@@ -0,0 +1,427 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newVideoAdConfig(db *gorm.DB, opts ...gen.DOOption) videoAdConfig {
_videoAdConfig := videoAdConfig{}
_videoAdConfig.videoAdConfigDo.UseDB(db, opts...)
_videoAdConfig.videoAdConfigDo.UseModel(&model.VideoAdConfig{})
tableName := _videoAdConfig.videoAdConfigDo.TableName()
_videoAdConfig.ALL = field.NewAsterisk(tableName)
_videoAdConfig.VideoID = field.NewString(tableName, "video_id")
_videoAdConfig.UserID = field.NewString(tableName, "user_id")
_videoAdConfig.AdTemplateID = field.NewString(tableName, "ad_template_id")
_videoAdConfig.VastTagURL = field.NewString(tableName, "vast_tag_url")
_videoAdConfig.AdFormat = field.NewString(tableName, "ad_format")
_videoAdConfig.Duration = field.NewInt64(tableName, "duration")
_videoAdConfig.CreatedAt = field.NewTime(tableName, "created_at")
_videoAdConfig.UpdatedAt = field.NewTime(tableName, "updated_at")
_videoAdConfig.Version = field.NewInt64(tableName, "version")
_videoAdConfig.fillFieldMap()
return _videoAdConfig
}
type videoAdConfig struct {
videoAdConfigDo videoAdConfigDo
ALL field.Asterisk
VideoID field.String
UserID field.String
AdTemplateID field.String
VastTagURL field.String
AdFormat field.String
Duration field.Int64
CreatedAt field.Time
UpdatedAt field.Time
Version field.Int64
fieldMap map[string]field.Expr
}
func (v videoAdConfig) Table(newTableName string) *videoAdConfig {
v.videoAdConfigDo.UseTable(newTableName)
return v.updateTableName(newTableName)
}
func (v videoAdConfig) As(alias string) *videoAdConfig {
v.videoAdConfigDo.DO = *(v.videoAdConfigDo.As(alias).(*gen.DO))
return v.updateTableName(alias)
}
func (v *videoAdConfig) updateTableName(table string) *videoAdConfig {
v.ALL = field.NewAsterisk(table)
v.VideoID = field.NewString(table, "video_id")
v.UserID = field.NewString(table, "user_id")
v.AdTemplateID = field.NewString(table, "ad_template_id")
v.VastTagURL = field.NewString(table, "vast_tag_url")
v.AdFormat = field.NewString(table, "ad_format")
v.Duration = field.NewInt64(table, "duration")
v.CreatedAt = field.NewTime(table, "created_at")
v.UpdatedAt = field.NewTime(table, "updated_at")
v.Version = field.NewInt64(table, "version")
v.fillFieldMap()
return v
}
func (v *videoAdConfig) WithContext(ctx context.Context) IVideoAdConfigDo {
return v.videoAdConfigDo.WithContext(ctx)
}
func (v videoAdConfig) TableName() string { return v.videoAdConfigDo.TableName() }
func (v videoAdConfig) Alias() string { return v.videoAdConfigDo.Alias() }
func (v videoAdConfig) Columns(cols ...field.Expr) gen.Columns {
return v.videoAdConfigDo.Columns(cols...)
}
func (v *videoAdConfig) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := v.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (v *videoAdConfig) fillFieldMap() {
v.fieldMap = make(map[string]field.Expr, 9)
v.fieldMap["video_id"] = v.VideoID
v.fieldMap["user_id"] = v.UserID
v.fieldMap["ad_template_id"] = v.AdTemplateID
v.fieldMap["vast_tag_url"] = v.VastTagURL
v.fieldMap["ad_format"] = v.AdFormat
v.fieldMap["duration"] = v.Duration
v.fieldMap["created_at"] = v.CreatedAt
v.fieldMap["updated_at"] = v.UpdatedAt
v.fieldMap["version"] = v.Version
}
func (v videoAdConfig) clone(db *gorm.DB) videoAdConfig {
v.videoAdConfigDo.ReplaceConnPool(db.Statement.ConnPool)
return v
}
func (v videoAdConfig) replaceDB(db *gorm.DB) videoAdConfig {
v.videoAdConfigDo.ReplaceDB(db)
return v
}
type videoAdConfigDo struct{ gen.DO }
type IVideoAdConfigDo interface {
gen.SubQuery
Debug() IVideoAdConfigDo
WithContext(ctx context.Context) IVideoAdConfigDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IVideoAdConfigDo
WriteDB() IVideoAdConfigDo
As(alias string) gen.Dao
Session(config *gorm.Session) IVideoAdConfigDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IVideoAdConfigDo
Not(conds ...gen.Condition) IVideoAdConfigDo
Or(conds ...gen.Condition) IVideoAdConfigDo
Select(conds ...field.Expr) IVideoAdConfigDo
Where(conds ...gen.Condition) IVideoAdConfigDo
Order(conds ...field.Expr) IVideoAdConfigDo
Distinct(cols ...field.Expr) IVideoAdConfigDo
Omit(cols ...field.Expr) IVideoAdConfigDo
Join(table schema.Tabler, on ...field.Expr) IVideoAdConfigDo
LeftJoin(table schema.Tabler, on ...field.Expr) IVideoAdConfigDo
RightJoin(table schema.Tabler, on ...field.Expr) IVideoAdConfigDo
Group(cols ...field.Expr) IVideoAdConfigDo
Having(conds ...gen.Condition) IVideoAdConfigDo
Limit(limit int) IVideoAdConfigDo
Offset(offset int) IVideoAdConfigDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IVideoAdConfigDo
Unscoped() IVideoAdConfigDo
Create(values ...*model.VideoAdConfig) error
CreateInBatches(values []*model.VideoAdConfig, batchSize int) error
Save(values ...*model.VideoAdConfig) error
First() (*model.VideoAdConfig, error)
Take() (*model.VideoAdConfig, error)
Last() (*model.VideoAdConfig, error)
Find() ([]*model.VideoAdConfig, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.VideoAdConfig, err error)
FindInBatches(result *[]*model.VideoAdConfig, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.VideoAdConfig) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IVideoAdConfigDo
Assign(attrs ...field.AssignExpr) IVideoAdConfigDo
Joins(fields ...field.RelationField) IVideoAdConfigDo
Preload(fields ...field.RelationField) IVideoAdConfigDo
FirstOrInit() (*model.VideoAdConfig, error)
FirstOrCreate() (*model.VideoAdConfig, error)
FindByPage(offset int, limit int) (result []*model.VideoAdConfig, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IVideoAdConfigDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (v videoAdConfigDo) Debug() IVideoAdConfigDo {
return v.withDO(v.DO.Debug())
}
func (v videoAdConfigDo) WithContext(ctx context.Context) IVideoAdConfigDo {
return v.withDO(v.DO.WithContext(ctx))
}
func (v videoAdConfigDo) ReadDB() IVideoAdConfigDo {
return v.Clauses(dbresolver.Read)
}
func (v videoAdConfigDo) WriteDB() IVideoAdConfigDo {
return v.Clauses(dbresolver.Write)
}
func (v videoAdConfigDo) Session(config *gorm.Session) IVideoAdConfigDo {
return v.withDO(v.DO.Session(config))
}
func (v videoAdConfigDo) Clauses(conds ...clause.Expression) IVideoAdConfigDo {
return v.withDO(v.DO.Clauses(conds...))
}
func (v videoAdConfigDo) Returning(value interface{}, columns ...string) IVideoAdConfigDo {
return v.withDO(v.DO.Returning(value, columns...))
}
func (v videoAdConfigDo) Not(conds ...gen.Condition) IVideoAdConfigDo {
return v.withDO(v.DO.Not(conds...))
}
func (v videoAdConfigDo) Or(conds ...gen.Condition) IVideoAdConfigDo {
return v.withDO(v.DO.Or(conds...))
}
func (v videoAdConfigDo) Select(conds ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.Select(conds...))
}
func (v videoAdConfigDo) Where(conds ...gen.Condition) IVideoAdConfigDo {
return v.withDO(v.DO.Where(conds...))
}
func (v videoAdConfigDo) Order(conds ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.Order(conds...))
}
func (v videoAdConfigDo) Distinct(cols ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.Distinct(cols...))
}
func (v videoAdConfigDo) Omit(cols ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.Omit(cols...))
}
func (v videoAdConfigDo) Join(table schema.Tabler, on ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.Join(table, on...))
}
func (v videoAdConfigDo) LeftJoin(table schema.Tabler, on ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.LeftJoin(table, on...))
}
func (v videoAdConfigDo) RightJoin(table schema.Tabler, on ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.RightJoin(table, on...))
}
func (v videoAdConfigDo) Group(cols ...field.Expr) IVideoAdConfigDo {
return v.withDO(v.DO.Group(cols...))
}
func (v videoAdConfigDo) Having(conds ...gen.Condition) IVideoAdConfigDo {
return v.withDO(v.DO.Having(conds...))
}
func (v videoAdConfigDo) Limit(limit int) IVideoAdConfigDo {
return v.withDO(v.DO.Limit(limit))
}
func (v videoAdConfigDo) Offset(offset int) IVideoAdConfigDo {
return v.withDO(v.DO.Offset(offset))
}
func (v videoAdConfigDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IVideoAdConfigDo {
return v.withDO(v.DO.Scopes(funcs...))
}
func (v videoAdConfigDo) Unscoped() IVideoAdConfigDo {
return v.withDO(v.DO.Unscoped())
}
func (v videoAdConfigDo) Create(values ...*model.VideoAdConfig) error {
if len(values) == 0 {
return nil
}
return v.DO.Create(values)
}
func (v videoAdConfigDo) CreateInBatches(values []*model.VideoAdConfig, batchSize int) error {
return v.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (v videoAdConfigDo) Save(values ...*model.VideoAdConfig) error {
if len(values) == 0 {
return nil
}
return v.DO.Save(values)
}
func (v videoAdConfigDo) First() (*model.VideoAdConfig, error) {
if result, err := v.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.VideoAdConfig), nil
}
}
func (v videoAdConfigDo) Take() (*model.VideoAdConfig, error) {
if result, err := v.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.VideoAdConfig), nil
}
}
func (v videoAdConfigDo) Last() (*model.VideoAdConfig, error) {
if result, err := v.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.VideoAdConfig), nil
}
}
func (v videoAdConfigDo) Find() ([]*model.VideoAdConfig, error) {
result, err := v.DO.Find()
return result.([]*model.VideoAdConfig), err
}
func (v videoAdConfigDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.VideoAdConfig, err error) {
buf := make([]*model.VideoAdConfig, 0, batchSize)
err = v.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (v videoAdConfigDo) FindInBatches(result *[]*model.VideoAdConfig, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return v.DO.FindInBatches(result, batchSize, fc)
}
func (v videoAdConfigDo) Attrs(attrs ...field.AssignExpr) IVideoAdConfigDo {
return v.withDO(v.DO.Attrs(attrs...))
}
func (v videoAdConfigDo) Assign(attrs ...field.AssignExpr) IVideoAdConfigDo {
return v.withDO(v.DO.Assign(attrs...))
}
func (v videoAdConfigDo) Joins(fields ...field.RelationField) IVideoAdConfigDo {
for _, _f := range fields {
v = *v.withDO(v.DO.Joins(_f))
}
return &v
}
func (v videoAdConfigDo) Preload(fields ...field.RelationField) IVideoAdConfigDo {
for _, _f := range fields {
v = *v.withDO(v.DO.Preload(_f))
}
return &v
}
func (v videoAdConfigDo) FirstOrInit() (*model.VideoAdConfig, error) {
if result, err := v.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.VideoAdConfig), nil
}
}
func (v videoAdConfigDo) FirstOrCreate() (*model.VideoAdConfig, error) {
if result, err := v.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.VideoAdConfig), nil
}
}
func (v videoAdConfigDo) FindByPage(offset int, limit int) (result []*model.VideoAdConfig, count int64, err error) {
result, err = v.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = v.Offset(-1).Limit(-1).Count()
return
}
func (v videoAdConfigDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = v.Count()
if err != nil {
return
}
err = v.Offset(offset).Limit(limit).Scan(result)
return
}
func (v videoAdConfigDo) Scan(result interface{}) (err error) {
return v.DO.Scan(result)
}
func (v videoAdConfigDo) Delete(models ...*model.VideoAdConfig) (result gen.ResultInfo, err error) {
return v.DO.Delete(models)
}
func (v *videoAdConfigDo) withDO(do gen.Dao) *videoAdConfigDo {
v.DO = *do.(*gen.DO)
return v
}

View File

@@ -0,0 +1,439 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"stream.api/internal/database/model"
)
func newWalletTransaction(db *gorm.DB, opts ...gen.DOOption) walletTransaction {
_walletTransaction := walletTransaction{}
_walletTransaction.walletTransactionDo.UseDB(db, opts...)
_walletTransaction.walletTransactionDo.UseModel(&model.WalletTransaction{})
tableName := _walletTransaction.walletTransactionDo.TableName()
_walletTransaction.ALL = field.NewAsterisk(tableName)
_walletTransaction.ID = field.NewString(tableName, "id")
_walletTransaction.UserID = field.NewString(tableName, "user_id")
_walletTransaction.Type = field.NewString(tableName, "type")
_walletTransaction.Amount = field.NewFloat64(tableName, "amount")
_walletTransaction.Currency = field.NewString(tableName, "currency")
_walletTransaction.Note = field.NewString(tableName, "note")
_walletTransaction.CreatedAt = field.NewTime(tableName, "created_at")
_walletTransaction.UpdatedAt = field.NewTime(tableName, "updated_at")
_walletTransaction.PaymentID = field.NewString(tableName, "payment_id")
_walletTransaction.PlanID = field.NewString(tableName, "plan_id")
_walletTransaction.TermMonths = field.NewInt32(tableName, "term_months")
_walletTransaction.Version = field.NewInt64(tableName, "version")
_walletTransaction.fillFieldMap()
return _walletTransaction
}
type walletTransaction struct {
walletTransactionDo walletTransactionDo
ALL field.Asterisk
ID field.String
UserID field.String
Type field.String
Amount field.Float64
Currency field.String
Note field.String
CreatedAt field.Time
UpdatedAt field.Time
PaymentID field.String
PlanID field.String
TermMonths field.Int32
Version field.Int64
fieldMap map[string]field.Expr
}
func (w walletTransaction) Table(newTableName string) *walletTransaction {
w.walletTransactionDo.UseTable(newTableName)
return w.updateTableName(newTableName)
}
func (w walletTransaction) As(alias string) *walletTransaction {
w.walletTransactionDo.DO = *(w.walletTransactionDo.As(alias).(*gen.DO))
return w.updateTableName(alias)
}
func (w *walletTransaction) updateTableName(table string) *walletTransaction {
w.ALL = field.NewAsterisk(table)
w.ID = field.NewString(table, "id")
w.UserID = field.NewString(table, "user_id")
w.Type = field.NewString(table, "type")
w.Amount = field.NewFloat64(table, "amount")
w.Currency = field.NewString(table, "currency")
w.Note = field.NewString(table, "note")
w.CreatedAt = field.NewTime(table, "created_at")
w.UpdatedAt = field.NewTime(table, "updated_at")
w.PaymentID = field.NewString(table, "payment_id")
w.PlanID = field.NewString(table, "plan_id")
w.TermMonths = field.NewInt32(table, "term_months")
w.Version = field.NewInt64(table, "version")
w.fillFieldMap()
return w
}
func (w *walletTransaction) WithContext(ctx context.Context) IWalletTransactionDo {
return w.walletTransactionDo.WithContext(ctx)
}
func (w walletTransaction) TableName() string { return w.walletTransactionDo.TableName() }
func (w walletTransaction) Alias() string { return w.walletTransactionDo.Alias() }
func (w walletTransaction) Columns(cols ...field.Expr) gen.Columns {
return w.walletTransactionDo.Columns(cols...)
}
func (w *walletTransaction) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := w.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (w *walletTransaction) fillFieldMap() {
w.fieldMap = make(map[string]field.Expr, 12)
w.fieldMap["id"] = w.ID
w.fieldMap["user_id"] = w.UserID
w.fieldMap["type"] = w.Type
w.fieldMap["amount"] = w.Amount
w.fieldMap["currency"] = w.Currency
w.fieldMap["note"] = w.Note
w.fieldMap["created_at"] = w.CreatedAt
w.fieldMap["updated_at"] = w.UpdatedAt
w.fieldMap["payment_id"] = w.PaymentID
w.fieldMap["plan_id"] = w.PlanID
w.fieldMap["term_months"] = w.TermMonths
w.fieldMap["version"] = w.Version
}
func (w walletTransaction) clone(db *gorm.DB) walletTransaction {
w.walletTransactionDo.ReplaceConnPool(db.Statement.ConnPool)
return w
}
func (w walletTransaction) replaceDB(db *gorm.DB) walletTransaction {
w.walletTransactionDo.ReplaceDB(db)
return w
}
type walletTransactionDo struct{ gen.DO }
type IWalletTransactionDo interface {
gen.SubQuery
Debug() IWalletTransactionDo
WithContext(ctx context.Context) IWalletTransactionDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IWalletTransactionDo
WriteDB() IWalletTransactionDo
As(alias string) gen.Dao
Session(config *gorm.Session) IWalletTransactionDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IWalletTransactionDo
Not(conds ...gen.Condition) IWalletTransactionDo
Or(conds ...gen.Condition) IWalletTransactionDo
Select(conds ...field.Expr) IWalletTransactionDo
Where(conds ...gen.Condition) IWalletTransactionDo
Order(conds ...field.Expr) IWalletTransactionDo
Distinct(cols ...field.Expr) IWalletTransactionDo
Omit(cols ...field.Expr) IWalletTransactionDo
Join(table schema.Tabler, on ...field.Expr) IWalletTransactionDo
LeftJoin(table schema.Tabler, on ...field.Expr) IWalletTransactionDo
RightJoin(table schema.Tabler, on ...field.Expr) IWalletTransactionDo
Group(cols ...field.Expr) IWalletTransactionDo
Having(conds ...gen.Condition) IWalletTransactionDo
Limit(limit int) IWalletTransactionDo
Offset(offset int) IWalletTransactionDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IWalletTransactionDo
Unscoped() IWalletTransactionDo
Create(values ...*model.WalletTransaction) error
CreateInBatches(values []*model.WalletTransaction, batchSize int) error
Save(values ...*model.WalletTransaction) error
First() (*model.WalletTransaction, error)
Take() (*model.WalletTransaction, error)
Last() (*model.WalletTransaction, error)
Find() ([]*model.WalletTransaction, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.WalletTransaction, err error)
FindInBatches(result *[]*model.WalletTransaction, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.WalletTransaction) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IWalletTransactionDo
Assign(attrs ...field.AssignExpr) IWalletTransactionDo
Joins(fields ...field.RelationField) IWalletTransactionDo
Preload(fields ...field.RelationField) IWalletTransactionDo
FirstOrInit() (*model.WalletTransaction, error)
FirstOrCreate() (*model.WalletTransaction, error)
FindByPage(offset int, limit int) (result []*model.WalletTransaction, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IWalletTransactionDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (w walletTransactionDo) Debug() IWalletTransactionDo {
return w.withDO(w.DO.Debug())
}
func (w walletTransactionDo) WithContext(ctx context.Context) IWalletTransactionDo {
return w.withDO(w.DO.WithContext(ctx))
}
func (w walletTransactionDo) ReadDB() IWalletTransactionDo {
return w.Clauses(dbresolver.Read)
}
func (w walletTransactionDo) WriteDB() IWalletTransactionDo {
return w.Clauses(dbresolver.Write)
}
func (w walletTransactionDo) Session(config *gorm.Session) IWalletTransactionDo {
return w.withDO(w.DO.Session(config))
}
func (w walletTransactionDo) Clauses(conds ...clause.Expression) IWalletTransactionDo {
return w.withDO(w.DO.Clauses(conds...))
}
func (w walletTransactionDo) Returning(value interface{}, columns ...string) IWalletTransactionDo {
return w.withDO(w.DO.Returning(value, columns...))
}
func (w walletTransactionDo) Not(conds ...gen.Condition) IWalletTransactionDo {
return w.withDO(w.DO.Not(conds...))
}
func (w walletTransactionDo) Or(conds ...gen.Condition) IWalletTransactionDo {
return w.withDO(w.DO.Or(conds...))
}
func (w walletTransactionDo) Select(conds ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.Select(conds...))
}
func (w walletTransactionDo) Where(conds ...gen.Condition) IWalletTransactionDo {
return w.withDO(w.DO.Where(conds...))
}
func (w walletTransactionDo) Order(conds ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.Order(conds...))
}
func (w walletTransactionDo) Distinct(cols ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.Distinct(cols...))
}
func (w walletTransactionDo) Omit(cols ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.Omit(cols...))
}
func (w walletTransactionDo) Join(table schema.Tabler, on ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.Join(table, on...))
}
func (w walletTransactionDo) LeftJoin(table schema.Tabler, on ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.LeftJoin(table, on...))
}
func (w walletTransactionDo) RightJoin(table schema.Tabler, on ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.RightJoin(table, on...))
}
func (w walletTransactionDo) Group(cols ...field.Expr) IWalletTransactionDo {
return w.withDO(w.DO.Group(cols...))
}
func (w walletTransactionDo) Having(conds ...gen.Condition) IWalletTransactionDo {
return w.withDO(w.DO.Having(conds...))
}
func (w walletTransactionDo) Limit(limit int) IWalletTransactionDo {
return w.withDO(w.DO.Limit(limit))
}
func (w walletTransactionDo) Offset(offset int) IWalletTransactionDo {
return w.withDO(w.DO.Offset(offset))
}
func (w walletTransactionDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IWalletTransactionDo {
return w.withDO(w.DO.Scopes(funcs...))
}
func (w walletTransactionDo) Unscoped() IWalletTransactionDo {
return w.withDO(w.DO.Unscoped())
}
func (w walletTransactionDo) Create(values ...*model.WalletTransaction) error {
if len(values) == 0 {
return nil
}
return w.DO.Create(values)
}
func (w walletTransactionDo) CreateInBatches(values []*model.WalletTransaction, batchSize int) error {
return w.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (w walletTransactionDo) Save(values ...*model.WalletTransaction) error {
if len(values) == 0 {
return nil
}
return w.DO.Save(values)
}
func (w walletTransactionDo) First() (*model.WalletTransaction, error) {
if result, err := w.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.WalletTransaction), nil
}
}
func (w walletTransactionDo) Take() (*model.WalletTransaction, error) {
if result, err := w.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.WalletTransaction), nil
}
}
func (w walletTransactionDo) Last() (*model.WalletTransaction, error) {
if result, err := w.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.WalletTransaction), nil
}
}
func (w walletTransactionDo) Find() ([]*model.WalletTransaction, error) {
result, err := w.DO.Find()
return result.([]*model.WalletTransaction), err
}
func (w walletTransactionDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.WalletTransaction, err error) {
buf := make([]*model.WalletTransaction, 0, batchSize)
err = w.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (w walletTransactionDo) FindInBatches(result *[]*model.WalletTransaction, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return w.DO.FindInBatches(result, batchSize, fc)
}
func (w walletTransactionDo) Attrs(attrs ...field.AssignExpr) IWalletTransactionDo {
return w.withDO(w.DO.Attrs(attrs...))
}
func (w walletTransactionDo) Assign(attrs ...field.AssignExpr) IWalletTransactionDo {
return w.withDO(w.DO.Assign(attrs...))
}
func (w walletTransactionDo) Joins(fields ...field.RelationField) IWalletTransactionDo {
for _, _f := range fields {
w = *w.withDO(w.DO.Joins(_f))
}
return &w
}
func (w walletTransactionDo) Preload(fields ...field.RelationField) IWalletTransactionDo {
for _, _f := range fields {
w = *w.withDO(w.DO.Preload(_f))
}
return &w
}
func (w walletTransactionDo) FirstOrInit() (*model.WalletTransaction, error) {
if result, err := w.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.WalletTransaction), nil
}
}
func (w walletTransactionDo) FirstOrCreate() (*model.WalletTransaction, error) {
if result, err := w.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.WalletTransaction), nil
}
}
func (w walletTransactionDo) FindByPage(offset int, limit int) (result []*model.WalletTransaction, count int64, err error) {
result, err = w.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = w.Offset(-1).Limit(-1).Count()
return
}
func (w walletTransactionDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = w.Count()
if err != nil {
return
}
err = w.Offset(offset).Limit(limit).Scan(result)
return
}
func (w walletTransactionDo) Scan(result interface{}) (err error) {
return w.DO.Scan(result)
}
func (w walletTransactionDo) Delete(models ...*model.WalletTransaction) (result gen.ResultInfo, err error) {
return w.DO.Delete(models)
}
func (w *walletTransactionDo) withDO(do gen.Dao) *walletTransactionDo {
w.DO = *do.(*gen.DO)
return w
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,731 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: app/v1/account.proto
package appv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
AccountService_GetMe_FullMethodName = "/stream.app.v1.AccountService/GetMe"
AccountService_UpdateMe_FullMethodName = "/stream.app.v1.AccountService/UpdateMe"
AccountService_DeleteMe_FullMethodName = "/stream.app.v1.AccountService/DeleteMe"
AccountService_ClearMyData_FullMethodName = "/stream.app.v1.AccountService/ClearMyData"
)
// AccountServiceClient is the client API for AccountService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AccountServiceClient interface {
GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error)
UpdateMe(ctx context.Context, in *UpdateMeRequest, opts ...grpc.CallOption) (*UpdateMeResponse, error)
DeleteMe(ctx context.Context, in *DeleteMeRequest, opts ...grpc.CallOption) (*MessageResponse, error)
ClearMyData(ctx context.Context, in *ClearMyDataRequest, opts ...grpc.CallOption) (*MessageResponse, error)
}
type accountServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAccountServiceClient(cc grpc.ClientConnInterface) AccountServiceClient {
return &accountServiceClient{cc}
}
func (c *accountServiceClient) GetMe(ctx context.Context, in *GetMeRequest, opts ...grpc.CallOption) (*GetMeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetMeResponse)
err := c.cc.Invoke(ctx, AccountService_GetMe_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *accountServiceClient) UpdateMe(ctx context.Context, in *UpdateMeRequest, opts ...grpc.CallOption) (*UpdateMeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateMeResponse)
err := c.cc.Invoke(ctx, AccountService_UpdateMe_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *accountServiceClient) DeleteMe(ctx context.Context, in *DeleteMeRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, AccountService_DeleteMe_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *accountServiceClient) ClearMyData(ctx context.Context, in *ClearMyDataRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, AccountService_ClearMyData_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AccountServiceServer is the server API for AccountService service.
// All implementations must embed UnimplementedAccountServiceServer
// for forward compatibility.
type AccountServiceServer interface {
GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error)
UpdateMe(context.Context, *UpdateMeRequest) (*UpdateMeResponse, error)
DeleteMe(context.Context, *DeleteMeRequest) (*MessageResponse, error)
ClearMyData(context.Context, *ClearMyDataRequest) (*MessageResponse, error)
mustEmbedUnimplementedAccountServiceServer()
}
// UnimplementedAccountServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAccountServiceServer struct{}
func (UnimplementedAccountServiceServer) GetMe(context.Context, *GetMeRequest) (*GetMeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMe not implemented")
}
func (UnimplementedAccountServiceServer) UpdateMe(context.Context, *UpdateMeRequest) (*UpdateMeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateMe not implemented")
}
func (UnimplementedAccountServiceServer) DeleteMe(context.Context, *DeleteMeRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteMe not implemented")
}
func (UnimplementedAccountServiceServer) ClearMyData(context.Context, *ClearMyDataRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClearMyData not implemented")
}
func (UnimplementedAccountServiceServer) mustEmbedUnimplementedAccountServiceServer() {}
func (UnimplementedAccountServiceServer) testEmbeddedByValue() {}
// UnsafeAccountServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AccountServiceServer will
// result in compilation errors.
type UnsafeAccountServiceServer interface {
mustEmbedUnimplementedAccountServiceServer()
}
func RegisterAccountServiceServer(s grpc.ServiceRegistrar, srv AccountServiceServer) {
// If the following call panics, it indicates UnimplementedAccountServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AccountService_ServiceDesc, srv)
}
func _AccountService_GetMe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountServiceServer).GetMe(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccountService_GetMe_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountServiceServer).GetMe(ctx, req.(*GetMeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AccountService_UpdateMe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountServiceServer).UpdateMe(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccountService_UpdateMe_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountServiceServer).UpdateMe(ctx, req.(*UpdateMeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AccountService_DeleteMe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteMeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountServiceServer).DeleteMe(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccountService_DeleteMe_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountServiceServer).DeleteMe(ctx, req.(*DeleteMeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AccountService_ClearMyData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearMyDataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountServiceServer).ClearMyData(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AccountService_ClearMyData_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountServiceServer).ClearMyData(ctx, req.(*ClearMyDataRequest))
}
return interceptor(ctx, in, info, handler)
}
// AccountService_ServiceDesc is the grpc.ServiceDesc for AccountService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AccountService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.AccountService",
HandlerType: (*AccountServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetMe",
Handler: _AccountService_GetMe_Handler,
},
{
MethodName: "UpdateMe",
Handler: _AccountService_UpdateMe_Handler,
},
{
MethodName: "DeleteMe",
Handler: _AccountService_DeleteMe_Handler,
},
{
MethodName: "ClearMyData",
Handler: _AccountService_ClearMyData_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/account.proto",
}
const (
PreferencesService_GetPreferences_FullMethodName = "/stream.app.v1.PreferencesService/GetPreferences"
PreferencesService_UpdatePreferences_FullMethodName = "/stream.app.v1.PreferencesService/UpdatePreferences"
)
// PreferencesServiceClient is the client API for PreferencesService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type PreferencesServiceClient interface {
GetPreferences(ctx context.Context, in *GetPreferencesRequest, opts ...grpc.CallOption) (*GetPreferencesResponse, error)
UpdatePreferences(ctx context.Context, in *UpdatePreferencesRequest, opts ...grpc.CallOption) (*UpdatePreferencesResponse, error)
}
type preferencesServiceClient struct {
cc grpc.ClientConnInterface
}
func NewPreferencesServiceClient(cc grpc.ClientConnInterface) PreferencesServiceClient {
return &preferencesServiceClient{cc}
}
func (c *preferencesServiceClient) GetPreferences(ctx context.Context, in *GetPreferencesRequest, opts ...grpc.CallOption) (*GetPreferencesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetPreferencesResponse)
err := c.cc.Invoke(ctx, PreferencesService_GetPreferences_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *preferencesServiceClient) UpdatePreferences(ctx context.Context, in *UpdatePreferencesRequest, opts ...grpc.CallOption) (*UpdatePreferencesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdatePreferencesResponse)
err := c.cc.Invoke(ctx, PreferencesService_UpdatePreferences_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// PreferencesServiceServer is the server API for PreferencesService service.
// All implementations must embed UnimplementedPreferencesServiceServer
// for forward compatibility.
type PreferencesServiceServer interface {
GetPreferences(context.Context, *GetPreferencesRequest) (*GetPreferencesResponse, error)
UpdatePreferences(context.Context, *UpdatePreferencesRequest) (*UpdatePreferencesResponse, error)
mustEmbedUnimplementedPreferencesServiceServer()
}
// UnimplementedPreferencesServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedPreferencesServiceServer struct{}
func (UnimplementedPreferencesServiceServer) GetPreferences(context.Context, *GetPreferencesRequest) (*GetPreferencesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetPreferences not implemented")
}
func (UnimplementedPreferencesServiceServer) UpdatePreferences(context.Context, *UpdatePreferencesRequest) (*UpdatePreferencesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdatePreferences not implemented")
}
func (UnimplementedPreferencesServiceServer) mustEmbedUnimplementedPreferencesServiceServer() {}
func (UnimplementedPreferencesServiceServer) testEmbeddedByValue() {}
// UnsafePreferencesServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to PreferencesServiceServer will
// result in compilation errors.
type UnsafePreferencesServiceServer interface {
mustEmbedUnimplementedPreferencesServiceServer()
}
func RegisterPreferencesServiceServer(s grpc.ServiceRegistrar, srv PreferencesServiceServer) {
// If the following call panics, it indicates UnimplementedPreferencesServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&PreferencesService_ServiceDesc, srv)
}
func _PreferencesService_GetPreferences_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetPreferencesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PreferencesServiceServer).GetPreferences(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PreferencesService_GetPreferences_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PreferencesServiceServer).GetPreferences(ctx, req.(*GetPreferencesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _PreferencesService_UpdatePreferences_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdatePreferencesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PreferencesServiceServer).UpdatePreferences(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PreferencesService_UpdatePreferences_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PreferencesServiceServer).UpdatePreferences(ctx, req.(*UpdatePreferencesRequest))
}
return interceptor(ctx, in, info, handler)
}
// PreferencesService_ServiceDesc is the grpc.ServiceDesc for PreferencesService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var PreferencesService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.PreferencesService",
HandlerType: (*PreferencesServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetPreferences",
Handler: _PreferencesService_GetPreferences_Handler,
},
{
MethodName: "UpdatePreferences",
Handler: _PreferencesService_UpdatePreferences_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/account.proto",
}
const (
UsageService_GetUsage_FullMethodName = "/stream.app.v1.UsageService/GetUsage"
)
// UsageServiceClient is the client API for UsageService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type UsageServiceClient interface {
GetUsage(ctx context.Context, in *GetUsageRequest, opts ...grpc.CallOption) (*GetUsageResponse, error)
}
type usageServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUsageServiceClient(cc grpc.ClientConnInterface) UsageServiceClient {
return &usageServiceClient{cc}
}
func (c *usageServiceClient) GetUsage(ctx context.Context, in *GetUsageRequest, opts ...grpc.CallOption) (*GetUsageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetUsageResponse)
err := c.cc.Invoke(ctx, UsageService_GetUsage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// UsageServiceServer is the server API for UsageService service.
// All implementations must embed UnimplementedUsageServiceServer
// for forward compatibility.
type UsageServiceServer interface {
GetUsage(context.Context, *GetUsageRequest) (*GetUsageResponse, error)
mustEmbedUnimplementedUsageServiceServer()
}
// UnimplementedUsageServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedUsageServiceServer struct{}
func (UnimplementedUsageServiceServer) GetUsage(context.Context, *GetUsageRequest) (*GetUsageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUsage not implemented")
}
func (UnimplementedUsageServiceServer) mustEmbedUnimplementedUsageServiceServer() {}
func (UnimplementedUsageServiceServer) testEmbeddedByValue() {}
// UnsafeUsageServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UsageServiceServer will
// result in compilation errors.
type UnsafeUsageServiceServer interface {
mustEmbedUnimplementedUsageServiceServer()
}
func RegisterUsageServiceServer(s grpc.ServiceRegistrar, srv UsageServiceServer) {
// If the following call panics, it indicates UnimplementedUsageServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&UsageService_ServiceDesc, srv)
}
func _UsageService_GetUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUsageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UsageServiceServer).GetUsage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UsageService_GetUsage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UsageServiceServer).GetUsage(ctx, req.(*GetUsageRequest))
}
return interceptor(ctx, in, info, handler)
}
// UsageService_ServiceDesc is the grpc.ServiceDesc for UsageService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var UsageService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.UsageService",
HandlerType: (*UsageServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetUsage",
Handler: _UsageService_GetUsage_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/account.proto",
}
const (
NotificationsService_ListNotifications_FullMethodName = "/stream.app.v1.NotificationsService/ListNotifications"
NotificationsService_MarkNotificationRead_FullMethodName = "/stream.app.v1.NotificationsService/MarkNotificationRead"
NotificationsService_MarkAllNotificationsRead_FullMethodName = "/stream.app.v1.NotificationsService/MarkAllNotificationsRead"
NotificationsService_DeleteNotification_FullMethodName = "/stream.app.v1.NotificationsService/DeleteNotification"
NotificationsService_ClearNotifications_FullMethodName = "/stream.app.v1.NotificationsService/ClearNotifications"
)
// NotificationsServiceClient is the client API for NotificationsService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type NotificationsServiceClient interface {
ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error)
MarkNotificationRead(ctx context.Context, in *MarkNotificationReadRequest, opts ...grpc.CallOption) (*MessageResponse, error)
MarkAllNotificationsRead(ctx context.Context, in *MarkAllNotificationsReadRequest, opts ...grpc.CallOption) (*MessageResponse, error)
DeleteNotification(ctx context.Context, in *DeleteNotificationRequest, opts ...grpc.CallOption) (*MessageResponse, error)
ClearNotifications(ctx context.Context, in *ClearNotificationsRequest, opts ...grpc.CallOption) (*MessageResponse, error)
}
type notificationsServiceClient struct {
cc grpc.ClientConnInterface
}
func NewNotificationsServiceClient(cc grpc.ClientConnInterface) NotificationsServiceClient {
return &notificationsServiceClient{cc}
}
func (c *notificationsServiceClient) ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListNotificationsResponse)
err := c.cc.Invoke(ctx, NotificationsService_ListNotifications_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notificationsServiceClient) MarkNotificationRead(ctx context.Context, in *MarkNotificationReadRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, NotificationsService_MarkNotificationRead_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notificationsServiceClient) MarkAllNotificationsRead(ctx context.Context, in *MarkAllNotificationsReadRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, NotificationsService_MarkAllNotificationsRead_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notificationsServiceClient) DeleteNotification(ctx context.Context, in *DeleteNotificationRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, NotificationsService_DeleteNotification_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notificationsServiceClient) ClearNotifications(ctx context.Context, in *ClearNotificationsRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, NotificationsService_ClearNotifications_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// NotificationsServiceServer is the server API for NotificationsService service.
// All implementations must embed UnimplementedNotificationsServiceServer
// for forward compatibility.
type NotificationsServiceServer interface {
ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error)
MarkNotificationRead(context.Context, *MarkNotificationReadRequest) (*MessageResponse, error)
MarkAllNotificationsRead(context.Context, *MarkAllNotificationsReadRequest) (*MessageResponse, error)
DeleteNotification(context.Context, *DeleteNotificationRequest) (*MessageResponse, error)
ClearNotifications(context.Context, *ClearNotificationsRequest) (*MessageResponse, error)
mustEmbedUnimplementedNotificationsServiceServer()
}
// UnimplementedNotificationsServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedNotificationsServiceServer struct{}
func (UnimplementedNotificationsServiceServer) ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListNotifications not implemented")
}
func (UnimplementedNotificationsServiceServer) MarkNotificationRead(context.Context, *MarkNotificationReadRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MarkNotificationRead not implemented")
}
func (UnimplementedNotificationsServiceServer) MarkAllNotificationsRead(context.Context, *MarkAllNotificationsReadRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MarkAllNotificationsRead not implemented")
}
func (UnimplementedNotificationsServiceServer) DeleteNotification(context.Context, *DeleteNotificationRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteNotification not implemented")
}
func (UnimplementedNotificationsServiceServer) ClearNotifications(context.Context, *ClearNotificationsRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClearNotifications not implemented")
}
func (UnimplementedNotificationsServiceServer) mustEmbedUnimplementedNotificationsServiceServer() {}
func (UnimplementedNotificationsServiceServer) testEmbeddedByValue() {}
// UnsafeNotificationsServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to NotificationsServiceServer will
// result in compilation errors.
type UnsafeNotificationsServiceServer interface {
mustEmbedUnimplementedNotificationsServiceServer()
}
func RegisterNotificationsServiceServer(s grpc.ServiceRegistrar, srv NotificationsServiceServer) {
// If the following call panics, it indicates UnimplementedNotificationsServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&NotificationsService_ServiceDesc, srv)
}
func _NotificationsService_ListNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListNotificationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationsServiceServer).ListNotifications(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationsService_ListNotifications_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationsServiceServer).ListNotifications(ctx, req.(*ListNotificationsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotificationsService_MarkNotificationRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MarkNotificationReadRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationsServiceServer).MarkNotificationRead(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationsService_MarkNotificationRead_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationsServiceServer).MarkNotificationRead(ctx, req.(*MarkNotificationReadRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotificationsService_MarkAllNotificationsRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MarkAllNotificationsReadRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationsServiceServer).MarkAllNotificationsRead(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationsService_MarkAllNotificationsRead_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationsServiceServer).MarkAllNotificationsRead(ctx, req.(*MarkAllNotificationsReadRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotificationsService_DeleteNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteNotificationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationsServiceServer).DeleteNotification(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationsService_DeleteNotification_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationsServiceServer).DeleteNotification(ctx, req.(*DeleteNotificationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotificationsService_ClearNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearNotificationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationsServiceServer).ClearNotifications(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationsService_ClearNotifications_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationsServiceServer).ClearNotifications(ctx, req.(*ClearNotificationsRequest))
}
return interceptor(ctx, in, info, handler)
}
// NotificationsService_ServiceDesc is the grpc.ServiceDesc for NotificationsService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var NotificationsService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.NotificationsService",
HandlerType: (*NotificationsServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListNotifications",
Handler: _NotificationsService_ListNotifications_Handler,
},
{
MethodName: "MarkNotificationRead",
Handler: _NotificationsService_MarkNotificationRead_Handler,
},
{
MethodName: "MarkAllNotificationsRead",
Handler: _NotificationsService_MarkAllNotificationsRead_Handler,
},
{
MethodName: "DeleteNotification",
Handler: _NotificationsService_DeleteNotification_Handler,
},
{
MethodName: "ClearNotifications",
Handler: _NotificationsService_ClearNotifications_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/account.proto",
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,697 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: app/v1/auth.proto
package appv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type LoginRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoginRequest) Reset() {
*x = LoginRequest{}
mi := &file_app_v1_auth_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginRequest) ProtoMessage() {}
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
func (*LoginRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{0}
}
func (x *LoginRequest) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *LoginRequest) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
type LoginResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoginResponse) Reset() {
*x = LoginResponse{}
mi := &file_app_v1_auth_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginResponse) ProtoMessage() {}
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
func (*LoginResponse) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{1}
}
func (x *LoginResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
type RegisterRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RegisterRequest) Reset() {
*x = RegisterRequest{}
mi := &file_app_v1_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RegisterRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RegisterRequest) ProtoMessage() {}
func (x *RegisterRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead.
func (*RegisterRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{2}
}
func (x *RegisterRequest) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *RegisterRequest) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *RegisterRequest) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
type RegisterResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RegisterResponse) Reset() {
*x = RegisterResponse{}
mi := &file_app_v1_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RegisterResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RegisterResponse) ProtoMessage() {}
func (x *RegisterResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead.
func (*RegisterResponse) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{3}
}
func (x *RegisterResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
type LogoutRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogoutRequest) Reset() {
*x = LogoutRequest{}
mi := &file_app_v1_auth_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogoutRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogoutRequest) ProtoMessage() {}
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead.
func (*LogoutRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{4}
}
type ChangePasswordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
CurrentPassword string `protobuf:"bytes,1,opt,name=current_password,json=currentPassword,proto3" json:"current_password,omitempty"`
NewPassword string `protobuf:"bytes,2,opt,name=new_password,json=newPassword,proto3" json:"new_password,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ChangePasswordRequest) Reset() {
*x = ChangePasswordRequest{}
mi := &file_app_v1_auth_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ChangePasswordRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ChangePasswordRequest) ProtoMessage() {}
func (x *ChangePasswordRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ChangePasswordRequest.ProtoReflect.Descriptor instead.
func (*ChangePasswordRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{5}
}
func (x *ChangePasswordRequest) GetCurrentPassword() string {
if x != nil {
return x.CurrentPassword
}
return ""
}
func (x *ChangePasswordRequest) GetNewPassword() string {
if x != nil {
return x.NewPassword
}
return ""
}
type ForgotPasswordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ForgotPasswordRequest) Reset() {
*x = ForgotPasswordRequest{}
mi := &file_app_v1_auth_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ForgotPasswordRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ForgotPasswordRequest) ProtoMessage() {}
func (x *ForgotPasswordRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ForgotPasswordRequest.ProtoReflect.Descriptor instead.
func (*ForgotPasswordRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{6}
}
func (x *ForgotPasswordRequest) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
type ResetPasswordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
NewPassword string `protobuf:"bytes,2,opt,name=new_password,json=newPassword,proto3" json:"new_password,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ResetPasswordRequest) Reset() {
*x = ResetPasswordRequest{}
mi := &file_app_v1_auth_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ResetPasswordRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResetPasswordRequest) ProtoMessage() {}
func (x *ResetPasswordRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ResetPasswordRequest.ProtoReflect.Descriptor instead.
func (*ResetPasswordRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{7}
}
func (x *ResetPasswordRequest) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
func (x *ResetPasswordRequest) GetNewPassword() string {
if x != nil {
return x.NewPassword
}
return ""
}
type GetGoogleLoginUrlRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetGoogleLoginUrlRequest) Reset() {
*x = GetGoogleLoginUrlRequest{}
mi := &file_app_v1_auth_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetGoogleLoginUrlRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetGoogleLoginUrlRequest) ProtoMessage() {}
func (x *GetGoogleLoginUrlRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetGoogleLoginUrlRequest.ProtoReflect.Descriptor instead.
func (*GetGoogleLoginUrlRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{8}
}
type GetGoogleLoginUrlResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetGoogleLoginUrlResponse) Reset() {
*x = GetGoogleLoginUrlResponse{}
mi := &file_app_v1_auth_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetGoogleLoginUrlResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetGoogleLoginUrlResponse) ProtoMessage() {}
func (x *GetGoogleLoginUrlResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetGoogleLoginUrlResponse.ProtoReflect.Descriptor instead.
func (*GetGoogleLoginUrlResponse) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{9}
}
func (x *GetGoogleLoginUrlResponse) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type CompleteGoogleLoginRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CompleteGoogleLoginRequest) Reset() {
*x = CompleteGoogleLoginRequest{}
mi := &file_app_v1_auth_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CompleteGoogleLoginRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CompleteGoogleLoginRequest) ProtoMessage() {}
func (x *CompleteGoogleLoginRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CompleteGoogleLoginRequest.ProtoReflect.Descriptor instead.
func (*CompleteGoogleLoginRequest) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{10}
}
func (x *CompleteGoogleLoginRequest) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
type CompleteGoogleLoginResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CompleteGoogleLoginResponse) Reset() {
*x = CompleteGoogleLoginResponse{}
mi := &file_app_v1_auth_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CompleteGoogleLoginResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CompleteGoogleLoginResponse) ProtoMessage() {}
func (x *CompleteGoogleLoginResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_auth_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CompleteGoogleLoginResponse.ProtoReflect.Descriptor instead.
func (*CompleteGoogleLoginResponse) Descriptor() ([]byte, []int) {
return file_app_v1_auth_proto_rawDescGZIP(), []int{11}
}
func (x *CompleteGoogleLoginResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
var File_app_v1_auth_proto protoreflect.FileDescriptor
const file_app_v1_auth_proto_rawDesc = "" +
"\n" +
"\x11app/v1/auth.proto\x12\rstream.app.v1\x1a\x13app/v1/common.proto\"@\n" +
"\fLoginRequest\x12\x14\n" +
"\x05email\x18\x01 \x01(\tR\x05email\x12\x1a\n" +
"\bpassword\x18\x02 \x01(\tR\bpassword\"8\n" +
"\rLoginResponse\x12'\n" +
"\x04user\x18\x01 \x01(\v2\x13.stream.app.v1.UserR\x04user\"_\n" +
"\x0fRegisterRequest\x12\x1a\n" +
"\busername\x18\x01 \x01(\tR\busername\x12\x14\n" +
"\x05email\x18\x02 \x01(\tR\x05email\x12\x1a\n" +
"\bpassword\x18\x03 \x01(\tR\bpassword\";\n" +
"\x10RegisterResponse\x12'\n" +
"\x04user\x18\x01 \x01(\v2\x13.stream.app.v1.UserR\x04user\"\x0f\n" +
"\rLogoutRequest\"e\n" +
"\x15ChangePasswordRequest\x12)\n" +
"\x10current_password\x18\x01 \x01(\tR\x0fcurrentPassword\x12!\n" +
"\fnew_password\x18\x02 \x01(\tR\vnewPassword\"-\n" +
"\x15ForgotPasswordRequest\x12\x14\n" +
"\x05email\x18\x01 \x01(\tR\x05email\"O\n" +
"\x14ResetPasswordRequest\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token\x12!\n" +
"\fnew_password\x18\x02 \x01(\tR\vnewPassword\"\x1a\n" +
"\x18GetGoogleLoginUrlRequest\"-\n" +
"\x19GetGoogleLoginUrlResponse\x12\x10\n" +
"\x03url\x18\x01 \x01(\tR\x03url\"0\n" +
"\x1aCompleteGoogleLoginRequest\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\"F\n" +
"\x1bCompleteGoogleLoginResponse\x12'\n" +
"\x04user\x18\x01 \x01(\v2\x13.stream.app.v1.UserR\x04user2\xc2\x05\n" +
"\vAuthService\x12B\n" +
"\x05Login\x12\x1b.stream.app.v1.LoginRequest\x1a\x1c.stream.app.v1.LoginResponse\x12K\n" +
"\bRegister\x12\x1e.stream.app.v1.RegisterRequest\x1a\x1f.stream.app.v1.RegisterResponse\x12F\n" +
"\x06Logout\x12\x1c.stream.app.v1.LogoutRequest\x1a\x1e.stream.app.v1.MessageResponse\x12V\n" +
"\x0eChangePassword\x12$.stream.app.v1.ChangePasswordRequest\x1a\x1e.stream.app.v1.MessageResponse\x12V\n" +
"\x0eForgotPassword\x12$.stream.app.v1.ForgotPasswordRequest\x1a\x1e.stream.app.v1.MessageResponse\x12T\n" +
"\rResetPassword\x12#.stream.app.v1.ResetPasswordRequest\x1a\x1e.stream.app.v1.MessageResponse\x12f\n" +
"\x11GetGoogleLoginUrl\x12'.stream.app.v1.GetGoogleLoginUrlRequest\x1a(.stream.app.v1.GetGoogleLoginUrlResponse\x12l\n" +
"\x13CompleteGoogleLogin\x12).stream.app.v1.CompleteGoogleLoginRequest\x1a*.stream.app.v1.CompleteGoogleLoginResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
var (
file_app_v1_auth_proto_rawDescOnce sync.Once
file_app_v1_auth_proto_rawDescData []byte
)
func file_app_v1_auth_proto_rawDescGZIP() []byte {
file_app_v1_auth_proto_rawDescOnce.Do(func() {
file_app_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_v1_auth_proto_rawDesc), len(file_app_v1_auth_proto_rawDesc)))
})
return file_app_v1_auth_proto_rawDescData
}
var file_app_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_app_v1_auth_proto_goTypes = []any{
(*LoginRequest)(nil), // 0: stream.app.v1.LoginRequest
(*LoginResponse)(nil), // 1: stream.app.v1.LoginResponse
(*RegisterRequest)(nil), // 2: stream.app.v1.RegisterRequest
(*RegisterResponse)(nil), // 3: stream.app.v1.RegisterResponse
(*LogoutRequest)(nil), // 4: stream.app.v1.LogoutRequest
(*ChangePasswordRequest)(nil), // 5: stream.app.v1.ChangePasswordRequest
(*ForgotPasswordRequest)(nil), // 6: stream.app.v1.ForgotPasswordRequest
(*ResetPasswordRequest)(nil), // 7: stream.app.v1.ResetPasswordRequest
(*GetGoogleLoginUrlRequest)(nil), // 8: stream.app.v1.GetGoogleLoginUrlRequest
(*GetGoogleLoginUrlResponse)(nil), // 9: stream.app.v1.GetGoogleLoginUrlResponse
(*CompleteGoogleLoginRequest)(nil), // 10: stream.app.v1.CompleteGoogleLoginRequest
(*CompleteGoogleLoginResponse)(nil), // 11: stream.app.v1.CompleteGoogleLoginResponse
(*User)(nil), // 12: stream.app.v1.User
(*MessageResponse)(nil), // 13: stream.app.v1.MessageResponse
}
var file_app_v1_auth_proto_depIdxs = []int32{
12, // 0: stream.app.v1.LoginResponse.user:type_name -> stream.app.v1.User
12, // 1: stream.app.v1.RegisterResponse.user:type_name -> stream.app.v1.User
12, // 2: stream.app.v1.CompleteGoogleLoginResponse.user:type_name -> stream.app.v1.User
0, // 3: stream.app.v1.AuthService.Login:input_type -> stream.app.v1.LoginRequest
2, // 4: stream.app.v1.AuthService.Register:input_type -> stream.app.v1.RegisterRequest
4, // 5: stream.app.v1.AuthService.Logout:input_type -> stream.app.v1.LogoutRequest
5, // 6: stream.app.v1.AuthService.ChangePassword:input_type -> stream.app.v1.ChangePasswordRequest
6, // 7: stream.app.v1.AuthService.ForgotPassword:input_type -> stream.app.v1.ForgotPasswordRequest
7, // 8: stream.app.v1.AuthService.ResetPassword:input_type -> stream.app.v1.ResetPasswordRequest
8, // 9: stream.app.v1.AuthService.GetGoogleLoginUrl:input_type -> stream.app.v1.GetGoogleLoginUrlRequest
10, // 10: stream.app.v1.AuthService.CompleteGoogleLogin:input_type -> stream.app.v1.CompleteGoogleLoginRequest
1, // 11: stream.app.v1.AuthService.Login:output_type -> stream.app.v1.LoginResponse
3, // 12: stream.app.v1.AuthService.Register:output_type -> stream.app.v1.RegisterResponse
13, // 13: stream.app.v1.AuthService.Logout:output_type -> stream.app.v1.MessageResponse
13, // 14: stream.app.v1.AuthService.ChangePassword:output_type -> stream.app.v1.MessageResponse
13, // 15: stream.app.v1.AuthService.ForgotPassword:output_type -> stream.app.v1.MessageResponse
13, // 16: stream.app.v1.AuthService.ResetPassword:output_type -> stream.app.v1.MessageResponse
9, // 17: stream.app.v1.AuthService.GetGoogleLoginUrl:output_type -> stream.app.v1.GetGoogleLoginUrlResponse
11, // 18: stream.app.v1.AuthService.CompleteGoogleLogin:output_type -> stream.app.v1.CompleteGoogleLoginResponse
11, // [11:19] is the sub-list for method output_type
3, // [3:11] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_app_v1_auth_proto_init() }
func file_app_v1_auth_proto_init() {
if File_app_v1_auth_proto != nil {
return
}
file_app_v1_common_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_auth_proto_rawDesc), len(file_app_v1_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_app_v1_auth_proto_goTypes,
DependencyIndexes: file_app_v1_auth_proto_depIdxs,
MessageInfos: file_app_v1_auth_proto_msgTypes,
}.Build()
File_app_v1_auth_proto = out.File
file_app_v1_auth_proto_goTypes = nil
file_app_v1_auth_proto_depIdxs = nil
}

View File

@@ -0,0 +1,387 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: app/v1/auth.proto
package appv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
AuthService_Login_FullMethodName = "/stream.app.v1.AuthService/Login"
AuthService_Register_FullMethodName = "/stream.app.v1.AuthService/Register"
AuthService_Logout_FullMethodName = "/stream.app.v1.AuthService/Logout"
AuthService_ChangePassword_FullMethodName = "/stream.app.v1.AuthService/ChangePassword"
AuthService_ForgotPassword_FullMethodName = "/stream.app.v1.AuthService/ForgotPassword"
AuthService_ResetPassword_FullMethodName = "/stream.app.v1.AuthService/ResetPassword"
AuthService_GetGoogleLoginUrl_FullMethodName = "/stream.app.v1.AuthService/GetGoogleLoginUrl"
AuthService_CompleteGoogleLogin_FullMethodName = "/stream.app.v1.AuthService/CompleteGoogleLogin"
)
// AuthServiceClient is the client API for AuthService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AuthServiceClient interface {
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error)
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*MessageResponse, error)
ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*MessageResponse, error)
ForgotPassword(ctx context.Context, in *ForgotPasswordRequest, opts ...grpc.CallOption) (*MessageResponse, error)
ResetPassword(ctx context.Context, in *ResetPasswordRequest, opts ...grpc.CallOption) (*MessageResponse, error)
GetGoogleLoginUrl(ctx context.Context, in *GetGoogleLoginUrlRequest, opts ...grpc.CallOption) (*GetGoogleLoginUrlResponse, error)
CompleteGoogleLogin(ctx context.Context, in *CompleteGoogleLoginRequest, opts ...grpc.CallOption) (*CompleteGoogleLoginResponse, error)
}
type authServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient {
return &authServiceClient{cc}
}
func (c *authServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LoginResponse)
err := c.cc.Invoke(ctx, AuthService_Login_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RegisterResponse)
err := c.cc.Invoke(ctx, AuthService_Register_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, AuthService_Logout_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, AuthService_ChangePassword_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) ForgotPassword(ctx context.Context, in *ForgotPasswordRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, AuthService_ForgotPassword_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) ResetPassword(ctx context.Context, in *ResetPasswordRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, AuthService_ResetPassword_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) GetGoogleLoginUrl(ctx context.Context, in *GetGoogleLoginUrlRequest, opts ...grpc.CallOption) (*GetGoogleLoginUrlResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetGoogleLoginUrlResponse)
err := c.cc.Invoke(ctx, AuthService_GetGoogleLoginUrl_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) CompleteGoogleLogin(ctx context.Context, in *CompleteGoogleLoginRequest, opts ...grpc.CallOption) (*CompleteGoogleLoginResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CompleteGoogleLoginResponse)
err := c.cc.Invoke(ctx, AuthService_CompleteGoogleLogin_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthServiceServer is the server API for AuthService service.
// All implementations must embed UnimplementedAuthServiceServer
// for forward compatibility.
type AuthServiceServer interface {
Login(context.Context, *LoginRequest) (*LoginResponse, error)
Register(context.Context, *RegisterRequest) (*RegisterResponse, error)
Logout(context.Context, *LogoutRequest) (*MessageResponse, error)
ChangePassword(context.Context, *ChangePasswordRequest) (*MessageResponse, error)
ForgotPassword(context.Context, *ForgotPasswordRequest) (*MessageResponse, error)
ResetPassword(context.Context, *ResetPasswordRequest) (*MessageResponse, error)
GetGoogleLoginUrl(context.Context, *GetGoogleLoginUrlRequest) (*GetGoogleLoginUrlResponse, error)
CompleteGoogleLogin(context.Context, *CompleteGoogleLoginRequest) (*CompleteGoogleLoginResponse, error)
mustEmbedUnimplementedAuthServiceServer()
}
// UnimplementedAuthServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAuthServiceServer struct{}
func (UnimplementedAuthServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Login not implemented")
}
func (UnimplementedAuthServiceServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Register not implemented")
}
func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Logout not implemented")
}
func (UnimplementedAuthServiceServer) ChangePassword(context.Context, *ChangePasswordRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ChangePassword not implemented")
}
func (UnimplementedAuthServiceServer) ForgotPassword(context.Context, *ForgotPasswordRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ForgotPassword not implemented")
}
func (UnimplementedAuthServiceServer) ResetPassword(context.Context, *ResetPasswordRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResetPassword not implemented")
}
func (UnimplementedAuthServiceServer) GetGoogleLoginUrl(context.Context, *GetGoogleLoginUrlRequest) (*GetGoogleLoginUrlResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetGoogleLoginUrl not implemented")
}
func (UnimplementedAuthServiceServer) CompleteGoogleLogin(context.Context, *CompleteGoogleLoginRequest) (*CompleteGoogleLoginResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CompleteGoogleLogin not implemented")
}
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AuthServiceServer will
// result in compilation errors.
type UnsafeAuthServiceServer interface {
mustEmbedUnimplementedAuthServiceServer()
}
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
// If the following call panics, it indicates UnimplementedAuthServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AuthService_ServiceDesc, srv)
}
func _AuthService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoginRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).Login(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_Login_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).Login(ctx, req.(*LoginRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).Register(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_Register_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).Register(ctx, req.(*RegisterRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LogoutRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).Logout(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_Logout_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).Logout(ctx, req.(*LogoutRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_ChangePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ChangePasswordRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).ChangePassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_ChangePassword_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).ChangePassword(ctx, req.(*ChangePasswordRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_ForgotPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ForgotPasswordRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).ForgotPassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_ForgotPassword_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).ForgotPassword(ctx, req.(*ForgotPasswordRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_ResetPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResetPasswordRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).ResetPassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_ResetPassword_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).ResetPassword(ctx, req.(*ResetPasswordRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_GetGoogleLoginUrl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetGoogleLoginUrlRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).GetGoogleLoginUrl(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_GetGoogleLoginUrl_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).GetGoogleLoginUrl(ctx, req.(*GetGoogleLoginUrlRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_CompleteGoogleLogin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CompleteGoogleLoginRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).CompleteGoogleLogin(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_CompleteGoogleLogin_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).CompleteGoogleLogin(ctx, req.(*CompleteGoogleLoginRequest))
}
return interceptor(ctx, in, info, handler)
}
// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AuthService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.AuthService",
HandlerType: (*AuthServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Login",
Handler: _AuthService_Login_Handler,
},
{
MethodName: "Register",
Handler: _AuthService_Register_Handler,
},
{
MethodName: "Logout",
Handler: _AuthService_Logout_Handler,
},
{
MethodName: "ChangePassword",
Handler: _AuthService_ChangePassword_Handler,
},
{
MethodName: "ForgotPassword",
Handler: _AuthService_ForgotPassword_Handler,
},
{
MethodName: "ResetPassword",
Handler: _AuthService_ResetPassword_Handler,
},
{
MethodName: "GetGoogleLoginUrl",
Handler: _AuthService_GetGoogleLoginUrl_Handler,
},
{
MethodName: "CompleteGoogleLogin",
Handler: _AuthService_CompleteGoogleLogin_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/auth.proto",
}

View File

@@ -0,0 +1,881 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: app/v1/catalog.proto
package appv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ListDomainsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDomainsRequest) Reset() {
*x = ListDomainsRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDomainsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDomainsRequest) ProtoMessage() {}
func (x *ListDomainsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDomainsRequest.ProtoReflect.Descriptor instead.
func (*ListDomainsRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{0}
}
type ListDomainsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Domains []*Domain `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListDomainsResponse) Reset() {
*x = ListDomainsResponse{}
mi := &file_app_v1_catalog_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListDomainsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListDomainsResponse) ProtoMessage() {}
func (x *ListDomainsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListDomainsResponse.ProtoReflect.Descriptor instead.
func (*ListDomainsResponse) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{1}
}
func (x *ListDomainsResponse) GetDomains() []*Domain {
if x != nil {
return x.Domains
}
return nil
}
type CreateDomainRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateDomainRequest) Reset() {
*x = CreateDomainRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateDomainRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateDomainRequest) ProtoMessage() {}
func (x *CreateDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateDomainRequest.ProtoReflect.Descriptor instead.
func (*CreateDomainRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{2}
}
func (x *CreateDomainRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type CreateDomainResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Domain *Domain `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateDomainResponse) Reset() {
*x = CreateDomainResponse{}
mi := &file_app_v1_catalog_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateDomainResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateDomainResponse) ProtoMessage() {}
func (x *CreateDomainResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateDomainResponse.ProtoReflect.Descriptor instead.
func (*CreateDomainResponse) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{3}
}
func (x *CreateDomainResponse) GetDomain() *Domain {
if x != nil {
return x.Domain
}
return nil
}
type DeleteDomainRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteDomainRequest) Reset() {
*x = DeleteDomainRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteDomainRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteDomainRequest) ProtoMessage() {}
func (x *DeleteDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteDomainRequest.ProtoReflect.Descriptor instead.
func (*DeleteDomainRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{4}
}
func (x *DeleteDomainRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type ListAdTemplatesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListAdTemplatesRequest) Reset() {
*x = ListAdTemplatesRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListAdTemplatesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListAdTemplatesRequest) ProtoMessage() {}
func (x *ListAdTemplatesRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListAdTemplatesRequest.ProtoReflect.Descriptor instead.
func (*ListAdTemplatesRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{5}
}
type ListAdTemplatesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Templates []*AdTemplate `protobuf:"bytes,1,rep,name=templates,proto3" json:"templates,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListAdTemplatesResponse) Reset() {
*x = ListAdTemplatesResponse{}
mi := &file_app_v1_catalog_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListAdTemplatesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListAdTemplatesResponse) ProtoMessage() {}
func (x *ListAdTemplatesResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListAdTemplatesResponse.ProtoReflect.Descriptor instead.
func (*ListAdTemplatesResponse) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{6}
}
func (x *ListAdTemplatesResponse) GetTemplates() []*AdTemplate {
if x != nil {
return x.Templates
}
return nil
}
type CreateAdTemplateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Description *string `protobuf:"bytes,2,opt,name=description,proto3,oneof" json:"description,omitempty"`
VastTagUrl string `protobuf:"bytes,3,opt,name=vast_tag_url,json=vastTagUrl,proto3" json:"vast_tag_url,omitempty"`
AdFormat *string `protobuf:"bytes,4,opt,name=ad_format,json=adFormat,proto3,oneof" json:"ad_format,omitempty"`
Duration *int32 `protobuf:"varint,5,opt,name=duration,proto3,oneof" json:"duration,omitempty"`
IsActive *bool `protobuf:"varint,6,opt,name=is_active,json=isActive,proto3,oneof" json:"is_active,omitempty"`
IsDefault *bool `protobuf:"varint,7,opt,name=is_default,json=isDefault,proto3,oneof" json:"is_default,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateAdTemplateRequest) Reset() {
*x = CreateAdTemplateRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateAdTemplateRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateAdTemplateRequest) ProtoMessage() {}
func (x *CreateAdTemplateRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateAdTemplateRequest.ProtoReflect.Descriptor instead.
func (*CreateAdTemplateRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{7}
}
func (x *CreateAdTemplateRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *CreateAdTemplateRequest) GetDescription() string {
if x != nil && x.Description != nil {
return *x.Description
}
return ""
}
func (x *CreateAdTemplateRequest) GetVastTagUrl() string {
if x != nil {
return x.VastTagUrl
}
return ""
}
func (x *CreateAdTemplateRequest) GetAdFormat() string {
if x != nil && x.AdFormat != nil {
return *x.AdFormat
}
return ""
}
func (x *CreateAdTemplateRequest) GetDuration() int32 {
if x != nil && x.Duration != nil {
return *x.Duration
}
return 0
}
func (x *CreateAdTemplateRequest) GetIsActive() bool {
if x != nil && x.IsActive != nil {
return *x.IsActive
}
return false
}
func (x *CreateAdTemplateRequest) GetIsDefault() bool {
if x != nil && x.IsDefault != nil {
return *x.IsDefault
}
return false
}
type CreateAdTemplateResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Template *AdTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateAdTemplateResponse) Reset() {
*x = CreateAdTemplateResponse{}
mi := &file_app_v1_catalog_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateAdTemplateResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateAdTemplateResponse) ProtoMessage() {}
func (x *CreateAdTemplateResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateAdTemplateResponse.ProtoReflect.Descriptor instead.
func (*CreateAdTemplateResponse) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{8}
}
func (x *CreateAdTemplateResponse) GetTemplate() *AdTemplate {
if x != nil {
return x.Template
}
return nil
}
type UpdateAdTemplateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Description *string `protobuf:"bytes,3,opt,name=description,proto3,oneof" json:"description,omitempty"`
VastTagUrl string `protobuf:"bytes,4,opt,name=vast_tag_url,json=vastTagUrl,proto3" json:"vast_tag_url,omitempty"`
AdFormat *string `protobuf:"bytes,5,opt,name=ad_format,json=adFormat,proto3,oneof" json:"ad_format,omitempty"`
Duration *int32 `protobuf:"varint,6,opt,name=duration,proto3,oneof" json:"duration,omitempty"`
IsActive *bool `protobuf:"varint,7,opt,name=is_active,json=isActive,proto3,oneof" json:"is_active,omitempty"`
IsDefault *bool `protobuf:"varint,8,opt,name=is_default,json=isDefault,proto3,oneof" json:"is_default,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateAdTemplateRequest) Reset() {
*x = UpdateAdTemplateRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateAdTemplateRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateAdTemplateRequest) ProtoMessage() {}
func (x *UpdateAdTemplateRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateAdTemplateRequest.ProtoReflect.Descriptor instead.
func (*UpdateAdTemplateRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{9}
}
func (x *UpdateAdTemplateRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *UpdateAdTemplateRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *UpdateAdTemplateRequest) GetDescription() string {
if x != nil && x.Description != nil {
return *x.Description
}
return ""
}
func (x *UpdateAdTemplateRequest) GetVastTagUrl() string {
if x != nil {
return x.VastTagUrl
}
return ""
}
func (x *UpdateAdTemplateRequest) GetAdFormat() string {
if x != nil && x.AdFormat != nil {
return *x.AdFormat
}
return ""
}
func (x *UpdateAdTemplateRequest) GetDuration() int32 {
if x != nil && x.Duration != nil {
return *x.Duration
}
return 0
}
func (x *UpdateAdTemplateRequest) GetIsActive() bool {
if x != nil && x.IsActive != nil {
return *x.IsActive
}
return false
}
func (x *UpdateAdTemplateRequest) GetIsDefault() bool {
if x != nil && x.IsDefault != nil {
return *x.IsDefault
}
return false
}
type UpdateAdTemplateResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Template *AdTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateAdTemplateResponse) Reset() {
*x = UpdateAdTemplateResponse{}
mi := &file_app_v1_catalog_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateAdTemplateResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateAdTemplateResponse) ProtoMessage() {}
func (x *UpdateAdTemplateResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateAdTemplateResponse.ProtoReflect.Descriptor instead.
func (*UpdateAdTemplateResponse) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{10}
}
func (x *UpdateAdTemplateResponse) GetTemplate() *AdTemplate {
if x != nil {
return x.Template
}
return nil
}
type DeleteAdTemplateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteAdTemplateRequest) Reset() {
*x = DeleteAdTemplateRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteAdTemplateRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteAdTemplateRequest) ProtoMessage() {}
func (x *DeleteAdTemplateRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteAdTemplateRequest.ProtoReflect.Descriptor instead.
func (*DeleteAdTemplateRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{11}
}
func (x *DeleteAdTemplateRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type ListPlansRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListPlansRequest) Reset() {
*x = ListPlansRequest{}
mi := &file_app_v1_catalog_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListPlansRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPlansRequest) ProtoMessage() {}
func (x *ListPlansRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListPlansRequest.ProtoReflect.Descriptor instead.
func (*ListPlansRequest) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{12}
}
type ListPlansResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Plans []*Plan `protobuf:"bytes,1,rep,name=plans,proto3" json:"plans,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListPlansResponse) Reset() {
*x = ListPlansResponse{}
mi := &file_app_v1_catalog_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListPlansResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPlansResponse) ProtoMessage() {}
func (x *ListPlansResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_catalog_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListPlansResponse.ProtoReflect.Descriptor instead.
func (*ListPlansResponse) Descriptor() ([]byte, []int) {
return file_app_v1_catalog_proto_rawDescGZIP(), []int{13}
}
func (x *ListPlansResponse) GetPlans() []*Plan {
if x != nil {
return x.Plans
}
return nil
}
var File_app_v1_catalog_proto protoreflect.FileDescriptor
const file_app_v1_catalog_proto_rawDesc = "" +
"\n" +
"\x14app/v1/catalog.proto\x12\rstream.app.v1\x1a\x13app/v1/common.proto\"\x14\n" +
"\x12ListDomainsRequest\"F\n" +
"\x13ListDomainsResponse\x12/\n" +
"\adomains\x18\x01 \x03(\v2\x15.stream.app.v1.DomainR\adomains\")\n" +
"\x13CreateDomainRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"E\n" +
"\x14CreateDomainResponse\x12-\n" +
"\x06domain\x18\x01 \x01(\v2\x15.stream.app.v1.DomainR\x06domain\"%\n" +
"\x13DeleteDomainRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"\x18\n" +
"\x16ListAdTemplatesRequest\"R\n" +
"\x17ListAdTemplatesResponse\x127\n" +
"\ttemplates\x18\x01 \x03(\v2\x19.stream.app.v1.AdTemplateR\ttemplates\"\xc7\x02\n" +
"\x17CreateAdTemplateRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12%\n" +
"\vdescription\x18\x02 \x01(\tH\x00R\vdescription\x88\x01\x01\x12 \n" +
"\fvast_tag_url\x18\x03 \x01(\tR\n" +
"vastTagUrl\x12 \n" +
"\tad_format\x18\x04 \x01(\tH\x01R\badFormat\x88\x01\x01\x12\x1f\n" +
"\bduration\x18\x05 \x01(\x05H\x02R\bduration\x88\x01\x01\x12 \n" +
"\tis_active\x18\x06 \x01(\bH\x03R\bisActive\x88\x01\x01\x12\"\n" +
"\n" +
"is_default\x18\a \x01(\bH\x04R\tisDefault\x88\x01\x01B\x0e\n" +
"\f_descriptionB\f\n" +
"\n" +
"_ad_formatB\v\n" +
"\t_durationB\f\n" +
"\n" +
"_is_activeB\r\n" +
"\v_is_default\"Q\n" +
"\x18CreateAdTemplateResponse\x125\n" +
"\btemplate\x18\x01 \x01(\v2\x19.stream.app.v1.AdTemplateR\btemplate\"\xd7\x02\n" +
"\x17UpdateAdTemplateRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12%\n" +
"\vdescription\x18\x03 \x01(\tH\x00R\vdescription\x88\x01\x01\x12 \n" +
"\fvast_tag_url\x18\x04 \x01(\tR\n" +
"vastTagUrl\x12 \n" +
"\tad_format\x18\x05 \x01(\tH\x01R\badFormat\x88\x01\x01\x12\x1f\n" +
"\bduration\x18\x06 \x01(\x05H\x02R\bduration\x88\x01\x01\x12 \n" +
"\tis_active\x18\a \x01(\bH\x03R\bisActive\x88\x01\x01\x12\"\n" +
"\n" +
"is_default\x18\b \x01(\bH\x04R\tisDefault\x88\x01\x01B\x0e\n" +
"\f_descriptionB\f\n" +
"\n" +
"_ad_formatB\v\n" +
"\t_durationB\f\n" +
"\n" +
"_is_activeB\r\n" +
"\v_is_default\"Q\n" +
"\x18UpdateAdTemplateResponse\x125\n" +
"\btemplate\x18\x01 \x01(\v2\x19.stream.app.v1.AdTemplateR\btemplate\")\n" +
"\x17DeleteAdTemplateRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"\x12\n" +
"\x10ListPlansRequest\">\n" +
"\x11ListPlansResponse\x12)\n" +
"\x05plans\x18\x01 \x03(\v2\x13.stream.app.v1.PlanR\x05plans2\x93\x02\n" +
"\x0eDomainsService\x12T\n" +
"\vListDomains\x12!.stream.app.v1.ListDomainsRequest\x1a\".stream.app.v1.ListDomainsResponse\x12W\n" +
"\fCreateDomain\x12\".stream.app.v1.CreateDomainRequest\x1a#.stream.app.v1.CreateDomainResponse\x12R\n" +
"\fDeleteDomain\x12\".stream.app.v1.DeleteDomainRequest\x1a\x1e.stream.app.v1.MessageResponse2\x9c\x03\n" +
"\x12AdTemplatesService\x12`\n" +
"\x0fListAdTemplates\x12%.stream.app.v1.ListAdTemplatesRequest\x1a&.stream.app.v1.ListAdTemplatesResponse\x12c\n" +
"\x10CreateAdTemplate\x12&.stream.app.v1.CreateAdTemplateRequest\x1a'.stream.app.v1.CreateAdTemplateResponse\x12c\n" +
"\x10UpdateAdTemplate\x12&.stream.app.v1.UpdateAdTemplateRequest\x1a'.stream.app.v1.UpdateAdTemplateResponse\x12Z\n" +
"\x10DeleteAdTemplate\x12&.stream.app.v1.DeleteAdTemplateRequest\x1a\x1e.stream.app.v1.MessageResponse2^\n" +
"\fPlansService\x12N\n" +
"\tListPlans\x12\x1f.stream.app.v1.ListPlansRequest\x1a .stream.app.v1.ListPlansResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
var (
file_app_v1_catalog_proto_rawDescOnce sync.Once
file_app_v1_catalog_proto_rawDescData []byte
)
func file_app_v1_catalog_proto_rawDescGZIP() []byte {
file_app_v1_catalog_proto_rawDescOnce.Do(func() {
file_app_v1_catalog_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_v1_catalog_proto_rawDesc), len(file_app_v1_catalog_proto_rawDesc)))
})
return file_app_v1_catalog_proto_rawDescData
}
var file_app_v1_catalog_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_app_v1_catalog_proto_goTypes = []any{
(*ListDomainsRequest)(nil), // 0: stream.app.v1.ListDomainsRequest
(*ListDomainsResponse)(nil), // 1: stream.app.v1.ListDomainsResponse
(*CreateDomainRequest)(nil), // 2: stream.app.v1.CreateDomainRequest
(*CreateDomainResponse)(nil), // 3: stream.app.v1.CreateDomainResponse
(*DeleteDomainRequest)(nil), // 4: stream.app.v1.DeleteDomainRequest
(*ListAdTemplatesRequest)(nil), // 5: stream.app.v1.ListAdTemplatesRequest
(*ListAdTemplatesResponse)(nil), // 6: stream.app.v1.ListAdTemplatesResponse
(*CreateAdTemplateRequest)(nil), // 7: stream.app.v1.CreateAdTemplateRequest
(*CreateAdTemplateResponse)(nil), // 8: stream.app.v1.CreateAdTemplateResponse
(*UpdateAdTemplateRequest)(nil), // 9: stream.app.v1.UpdateAdTemplateRequest
(*UpdateAdTemplateResponse)(nil), // 10: stream.app.v1.UpdateAdTemplateResponse
(*DeleteAdTemplateRequest)(nil), // 11: stream.app.v1.DeleteAdTemplateRequest
(*ListPlansRequest)(nil), // 12: stream.app.v1.ListPlansRequest
(*ListPlansResponse)(nil), // 13: stream.app.v1.ListPlansResponse
(*Domain)(nil), // 14: stream.app.v1.Domain
(*AdTemplate)(nil), // 15: stream.app.v1.AdTemplate
(*Plan)(nil), // 16: stream.app.v1.Plan
(*MessageResponse)(nil), // 17: stream.app.v1.MessageResponse
}
var file_app_v1_catalog_proto_depIdxs = []int32{
14, // 0: stream.app.v1.ListDomainsResponse.domains:type_name -> stream.app.v1.Domain
14, // 1: stream.app.v1.CreateDomainResponse.domain:type_name -> stream.app.v1.Domain
15, // 2: stream.app.v1.ListAdTemplatesResponse.templates:type_name -> stream.app.v1.AdTemplate
15, // 3: stream.app.v1.CreateAdTemplateResponse.template:type_name -> stream.app.v1.AdTemplate
15, // 4: stream.app.v1.UpdateAdTemplateResponse.template:type_name -> stream.app.v1.AdTemplate
16, // 5: stream.app.v1.ListPlansResponse.plans:type_name -> stream.app.v1.Plan
0, // 6: stream.app.v1.DomainsService.ListDomains:input_type -> stream.app.v1.ListDomainsRequest
2, // 7: stream.app.v1.DomainsService.CreateDomain:input_type -> stream.app.v1.CreateDomainRequest
4, // 8: stream.app.v1.DomainsService.DeleteDomain:input_type -> stream.app.v1.DeleteDomainRequest
5, // 9: stream.app.v1.AdTemplatesService.ListAdTemplates:input_type -> stream.app.v1.ListAdTemplatesRequest
7, // 10: stream.app.v1.AdTemplatesService.CreateAdTemplate:input_type -> stream.app.v1.CreateAdTemplateRequest
9, // 11: stream.app.v1.AdTemplatesService.UpdateAdTemplate:input_type -> stream.app.v1.UpdateAdTemplateRequest
11, // 12: stream.app.v1.AdTemplatesService.DeleteAdTemplate:input_type -> stream.app.v1.DeleteAdTemplateRequest
12, // 13: stream.app.v1.PlansService.ListPlans:input_type -> stream.app.v1.ListPlansRequest
1, // 14: stream.app.v1.DomainsService.ListDomains:output_type -> stream.app.v1.ListDomainsResponse
3, // 15: stream.app.v1.DomainsService.CreateDomain:output_type -> stream.app.v1.CreateDomainResponse
17, // 16: stream.app.v1.DomainsService.DeleteDomain:output_type -> stream.app.v1.MessageResponse
6, // 17: stream.app.v1.AdTemplatesService.ListAdTemplates:output_type -> stream.app.v1.ListAdTemplatesResponse
8, // 18: stream.app.v1.AdTemplatesService.CreateAdTemplate:output_type -> stream.app.v1.CreateAdTemplateResponse
10, // 19: stream.app.v1.AdTemplatesService.UpdateAdTemplate:output_type -> stream.app.v1.UpdateAdTemplateResponse
17, // 20: stream.app.v1.AdTemplatesService.DeleteAdTemplate:output_type -> stream.app.v1.MessageResponse
13, // 21: stream.app.v1.PlansService.ListPlans:output_type -> stream.app.v1.ListPlansResponse
14, // [14:22] is the sub-list for method output_type
6, // [6:14] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_app_v1_catalog_proto_init() }
func file_app_v1_catalog_proto_init() {
if File_app_v1_catalog_proto != nil {
return
}
file_app_v1_common_proto_init()
file_app_v1_catalog_proto_msgTypes[7].OneofWrappers = []any{}
file_app_v1_catalog_proto_msgTypes[9].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_catalog_proto_rawDesc), len(file_app_v1_catalog_proto_rawDesc)),
NumEnums: 0,
NumMessages: 14,
NumExtensions: 0,
NumServices: 3,
},
GoTypes: file_app_v1_catalog_proto_goTypes,
DependencyIndexes: file_app_v1_catalog_proto_depIdxs,
MessageInfos: file_app_v1_catalog_proto_msgTypes,
}.Build()
File_app_v1_catalog_proto = out.File
file_app_v1_catalog_proto_goTypes = nil
file_app_v1_catalog_proto_depIdxs = nil
}

View File

@@ -0,0 +1,515 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: app/v1/catalog.proto
package appv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
DomainsService_ListDomains_FullMethodName = "/stream.app.v1.DomainsService/ListDomains"
DomainsService_CreateDomain_FullMethodName = "/stream.app.v1.DomainsService/CreateDomain"
DomainsService_DeleteDomain_FullMethodName = "/stream.app.v1.DomainsService/DeleteDomain"
)
// DomainsServiceClient is the client API for DomainsService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DomainsServiceClient interface {
ListDomains(ctx context.Context, in *ListDomainsRequest, opts ...grpc.CallOption) (*ListDomainsResponse, error)
CreateDomain(ctx context.Context, in *CreateDomainRequest, opts ...grpc.CallOption) (*CreateDomainResponse, error)
DeleteDomain(ctx context.Context, in *DeleteDomainRequest, opts ...grpc.CallOption) (*MessageResponse, error)
}
type domainsServiceClient struct {
cc grpc.ClientConnInterface
}
func NewDomainsServiceClient(cc grpc.ClientConnInterface) DomainsServiceClient {
return &domainsServiceClient{cc}
}
func (c *domainsServiceClient) ListDomains(ctx context.Context, in *ListDomainsRequest, opts ...grpc.CallOption) (*ListDomainsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListDomainsResponse)
err := c.cc.Invoke(ctx, DomainsService_ListDomains_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *domainsServiceClient) CreateDomain(ctx context.Context, in *CreateDomainRequest, opts ...grpc.CallOption) (*CreateDomainResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateDomainResponse)
err := c.cc.Invoke(ctx, DomainsService_CreateDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *domainsServiceClient) DeleteDomain(ctx context.Context, in *DeleteDomainRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, DomainsService_DeleteDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// DomainsServiceServer is the server API for DomainsService service.
// All implementations must embed UnimplementedDomainsServiceServer
// for forward compatibility.
type DomainsServiceServer interface {
ListDomains(context.Context, *ListDomainsRequest) (*ListDomainsResponse, error)
CreateDomain(context.Context, *CreateDomainRequest) (*CreateDomainResponse, error)
DeleteDomain(context.Context, *DeleteDomainRequest) (*MessageResponse, error)
mustEmbedUnimplementedDomainsServiceServer()
}
// UnimplementedDomainsServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedDomainsServiceServer struct{}
func (UnimplementedDomainsServiceServer) ListDomains(context.Context, *ListDomainsRequest) (*ListDomainsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListDomains not implemented")
}
func (UnimplementedDomainsServiceServer) CreateDomain(context.Context, *CreateDomainRequest) (*CreateDomainResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateDomain not implemented")
}
func (UnimplementedDomainsServiceServer) DeleteDomain(context.Context, *DeleteDomainRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteDomain not implemented")
}
func (UnimplementedDomainsServiceServer) mustEmbedUnimplementedDomainsServiceServer() {}
func (UnimplementedDomainsServiceServer) testEmbeddedByValue() {}
// UnsafeDomainsServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DomainsServiceServer will
// result in compilation errors.
type UnsafeDomainsServiceServer interface {
mustEmbedUnimplementedDomainsServiceServer()
}
func RegisterDomainsServiceServer(s grpc.ServiceRegistrar, srv DomainsServiceServer) {
// If the following call panics, it indicates UnimplementedDomainsServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&DomainsService_ServiceDesc, srv)
}
func _DomainsService_ListDomains_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListDomainsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DomainsServiceServer).ListDomains(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DomainsService_ListDomains_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DomainsServiceServer).ListDomains(ctx, req.(*ListDomainsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DomainsService_CreateDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateDomainRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DomainsServiceServer).CreateDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DomainsService_CreateDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DomainsServiceServer).CreateDomain(ctx, req.(*CreateDomainRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DomainsService_DeleteDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteDomainRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DomainsServiceServer).DeleteDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DomainsService_DeleteDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DomainsServiceServer).DeleteDomain(ctx, req.(*DeleteDomainRequest))
}
return interceptor(ctx, in, info, handler)
}
// DomainsService_ServiceDesc is the grpc.ServiceDesc for DomainsService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var DomainsService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.DomainsService",
HandlerType: (*DomainsServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListDomains",
Handler: _DomainsService_ListDomains_Handler,
},
{
MethodName: "CreateDomain",
Handler: _DomainsService_CreateDomain_Handler,
},
{
MethodName: "DeleteDomain",
Handler: _DomainsService_DeleteDomain_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/catalog.proto",
}
const (
AdTemplatesService_ListAdTemplates_FullMethodName = "/stream.app.v1.AdTemplatesService/ListAdTemplates"
AdTemplatesService_CreateAdTemplate_FullMethodName = "/stream.app.v1.AdTemplatesService/CreateAdTemplate"
AdTemplatesService_UpdateAdTemplate_FullMethodName = "/stream.app.v1.AdTemplatesService/UpdateAdTemplate"
AdTemplatesService_DeleteAdTemplate_FullMethodName = "/stream.app.v1.AdTemplatesService/DeleteAdTemplate"
)
// AdTemplatesServiceClient is the client API for AdTemplatesService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AdTemplatesServiceClient interface {
ListAdTemplates(ctx context.Context, in *ListAdTemplatesRequest, opts ...grpc.CallOption) (*ListAdTemplatesResponse, error)
CreateAdTemplate(ctx context.Context, in *CreateAdTemplateRequest, opts ...grpc.CallOption) (*CreateAdTemplateResponse, error)
UpdateAdTemplate(ctx context.Context, in *UpdateAdTemplateRequest, opts ...grpc.CallOption) (*UpdateAdTemplateResponse, error)
DeleteAdTemplate(ctx context.Context, in *DeleteAdTemplateRequest, opts ...grpc.CallOption) (*MessageResponse, error)
}
type adTemplatesServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAdTemplatesServiceClient(cc grpc.ClientConnInterface) AdTemplatesServiceClient {
return &adTemplatesServiceClient{cc}
}
func (c *adTemplatesServiceClient) ListAdTemplates(ctx context.Context, in *ListAdTemplatesRequest, opts ...grpc.CallOption) (*ListAdTemplatesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListAdTemplatesResponse)
err := c.cc.Invoke(ctx, AdTemplatesService_ListAdTemplates_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adTemplatesServiceClient) CreateAdTemplate(ctx context.Context, in *CreateAdTemplateRequest, opts ...grpc.CallOption) (*CreateAdTemplateResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateAdTemplateResponse)
err := c.cc.Invoke(ctx, AdTemplatesService_CreateAdTemplate_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adTemplatesServiceClient) UpdateAdTemplate(ctx context.Context, in *UpdateAdTemplateRequest, opts ...grpc.CallOption) (*UpdateAdTemplateResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateAdTemplateResponse)
err := c.cc.Invoke(ctx, AdTemplatesService_UpdateAdTemplate_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adTemplatesServiceClient) DeleteAdTemplate(ctx context.Context, in *DeleteAdTemplateRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, AdTemplatesService_DeleteAdTemplate_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AdTemplatesServiceServer is the server API for AdTemplatesService service.
// All implementations must embed UnimplementedAdTemplatesServiceServer
// for forward compatibility.
type AdTemplatesServiceServer interface {
ListAdTemplates(context.Context, *ListAdTemplatesRequest) (*ListAdTemplatesResponse, error)
CreateAdTemplate(context.Context, *CreateAdTemplateRequest) (*CreateAdTemplateResponse, error)
UpdateAdTemplate(context.Context, *UpdateAdTemplateRequest) (*UpdateAdTemplateResponse, error)
DeleteAdTemplate(context.Context, *DeleteAdTemplateRequest) (*MessageResponse, error)
mustEmbedUnimplementedAdTemplatesServiceServer()
}
// UnimplementedAdTemplatesServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAdTemplatesServiceServer struct{}
func (UnimplementedAdTemplatesServiceServer) ListAdTemplates(context.Context, *ListAdTemplatesRequest) (*ListAdTemplatesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAdTemplates not implemented")
}
func (UnimplementedAdTemplatesServiceServer) CreateAdTemplate(context.Context, *CreateAdTemplateRequest) (*CreateAdTemplateResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateAdTemplate not implemented")
}
func (UnimplementedAdTemplatesServiceServer) UpdateAdTemplate(context.Context, *UpdateAdTemplateRequest) (*UpdateAdTemplateResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateAdTemplate not implemented")
}
func (UnimplementedAdTemplatesServiceServer) DeleteAdTemplate(context.Context, *DeleteAdTemplateRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteAdTemplate not implemented")
}
func (UnimplementedAdTemplatesServiceServer) mustEmbedUnimplementedAdTemplatesServiceServer() {}
func (UnimplementedAdTemplatesServiceServer) testEmbeddedByValue() {}
// UnsafeAdTemplatesServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AdTemplatesServiceServer will
// result in compilation errors.
type UnsafeAdTemplatesServiceServer interface {
mustEmbedUnimplementedAdTemplatesServiceServer()
}
func RegisterAdTemplatesServiceServer(s grpc.ServiceRegistrar, srv AdTemplatesServiceServer) {
// If the following call panics, it indicates UnimplementedAdTemplatesServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AdTemplatesService_ServiceDesc, srv)
}
func _AdTemplatesService_ListAdTemplates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAdTemplatesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdTemplatesServiceServer).ListAdTemplates(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdTemplatesService_ListAdTemplates_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdTemplatesServiceServer).ListAdTemplates(ctx, req.(*ListAdTemplatesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdTemplatesService_CreateAdTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateAdTemplateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdTemplatesServiceServer).CreateAdTemplate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdTemplatesService_CreateAdTemplate_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdTemplatesServiceServer).CreateAdTemplate(ctx, req.(*CreateAdTemplateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdTemplatesService_UpdateAdTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateAdTemplateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdTemplatesServiceServer).UpdateAdTemplate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdTemplatesService_UpdateAdTemplate_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdTemplatesServiceServer).UpdateAdTemplate(ctx, req.(*UpdateAdTemplateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdTemplatesService_DeleteAdTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteAdTemplateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdTemplatesServiceServer).DeleteAdTemplate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdTemplatesService_DeleteAdTemplate_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdTemplatesServiceServer).DeleteAdTemplate(ctx, req.(*DeleteAdTemplateRequest))
}
return interceptor(ctx, in, info, handler)
}
// AdTemplatesService_ServiceDesc is the grpc.ServiceDesc for AdTemplatesService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AdTemplatesService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.AdTemplatesService",
HandlerType: (*AdTemplatesServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListAdTemplates",
Handler: _AdTemplatesService_ListAdTemplates_Handler,
},
{
MethodName: "CreateAdTemplate",
Handler: _AdTemplatesService_CreateAdTemplate_Handler,
},
{
MethodName: "UpdateAdTemplate",
Handler: _AdTemplatesService_UpdateAdTemplate_Handler,
},
{
MethodName: "DeleteAdTemplate",
Handler: _AdTemplatesService_DeleteAdTemplate_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/catalog.proto",
}
const (
PlansService_ListPlans_FullMethodName = "/stream.app.v1.PlansService/ListPlans"
)
// PlansServiceClient is the client API for PlansService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type PlansServiceClient interface {
ListPlans(ctx context.Context, in *ListPlansRequest, opts ...grpc.CallOption) (*ListPlansResponse, error)
}
type plansServiceClient struct {
cc grpc.ClientConnInterface
}
func NewPlansServiceClient(cc grpc.ClientConnInterface) PlansServiceClient {
return &plansServiceClient{cc}
}
func (c *plansServiceClient) ListPlans(ctx context.Context, in *ListPlansRequest, opts ...grpc.CallOption) (*ListPlansResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListPlansResponse)
err := c.cc.Invoke(ctx, PlansService_ListPlans_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// PlansServiceServer is the server API for PlansService service.
// All implementations must embed UnimplementedPlansServiceServer
// for forward compatibility.
type PlansServiceServer interface {
ListPlans(context.Context, *ListPlansRequest) (*ListPlansResponse, error)
mustEmbedUnimplementedPlansServiceServer()
}
// UnimplementedPlansServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedPlansServiceServer struct{}
func (UnimplementedPlansServiceServer) ListPlans(context.Context, *ListPlansRequest) (*ListPlansResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListPlans not implemented")
}
func (UnimplementedPlansServiceServer) mustEmbedUnimplementedPlansServiceServer() {}
func (UnimplementedPlansServiceServer) testEmbeddedByValue() {}
// UnsafePlansServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to PlansServiceServer will
// result in compilation errors.
type UnsafePlansServiceServer interface {
mustEmbedUnimplementedPlansServiceServer()
}
func RegisterPlansServiceServer(s grpc.ServiceRegistrar, srv PlansServiceServer) {
// If the following call panics, it indicates UnimplementedPlansServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&PlansService_ServiceDesc, srv)
}
func _PlansService_ListPlans_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPlansRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PlansServiceServer).ListPlans(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PlansService_ListPlans_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PlansServiceServer).ListPlans(ctx, req.(*ListPlansRequest))
}
return interceptor(ctx, in, info, handler)
}
// PlansService_ServiceDesc is the grpc.ServiceDesc for PlansService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var PlansService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.PlansService",
HandlerType: (*PlansServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListPlans",
Handler: _PlansService_ListPlans_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/catalog.proto",
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,568 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: app/v1/payments.proto
package appv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type CreatePaymentRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
PlanId string `protobuf:"bytes,1,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"`
TermMonths int32 `protobuf:"varint,2,opt,name=term_months,json=termMonths,proto3" json:"term_months,omitempty"`
PaymentMethod string `protobuf:"bytes,3,opt,name=payment_method,json=paymentMethod,proto3" json:"payment_method,omitempty"`
TopupAmount *float64 `protobuf:"fixed64,4,opt,name=topup_amount,json=topupAmount,proto3,oneof" json:"topup_amount,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreatePaymentRequest) Reset() {
*x = CreatePaymentRequest{}
mi := &file_app_v1_payments_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreatePaymentRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreatePaymentRequest) ProtoMessage() {}
func (x *CreatePaymentRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreatePaymentRequest.ProtoReflect.Descriptor instead.
func (*CreatePaymentRequest) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{0}
}
func (x *CreatePaymentRequest) GetPlanId() string {
if x != nil {
return x.PlanId
}
return ""
}
func (x *CreatePaymentRequest) GetTermMonths() int32 {
if x != nil {
return x.TermMonths
}
return 0
}
func (x *CreatePaymentRequest) GetPaymentMethod() string {
if x != nil {
return x.PaymentMethod
}
return ""
}
func (x *CreatePaymentRequest) GetTopupAmount() float64 {
if x != nil && x.TopupAmount != nil {
return *x.TopupAmount
}
return 0
}
type CreatePaymentResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Payment *Payment `protobuf:"bytes,1,opt,name=payment,proto3" json:"payment,omitempty"`
Subscription *PlanSubscription `protobuf:"bytes,2,opt,name=subscription,proto3" json:"subscription,omitempty"`
WalletBalance float64 `protobuf:"fixed64,3,opt,name=wallet_balance,json=walletBalance,proto3" json:"wallet_balance,omitempty"`
InvoiceId string `protobuf:"bytes,4,opt,name=invoice_id,json=invoiceId,proto3" json:"invoice_id,omitempty"`
Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreatePaymentResponse) Reset() {
*x = CreatePaymentResponse{}
mi := &file_app_v1_payments_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreatePaymentResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreatePaymentResponse) ProtoMessage() {}
func (x *CreatePaymentResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreatePaymentResponse.ProtoReflect.Descriptor instead.
func (*CreatePaymentResponse) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{1}
}
func (x *CreatePaymentResponse) GetPayment() *Payment {
if x != nil {
return x.Payment
}
return nil
}
func (x *CreatePaymentResponse) GetSubscription() *PlanSubscription {
if x != nil {
return x.Subscription
}
return nil
}
func (x *CreatePaymentResponse) GetWalletBalance() float64 {
if x != nil {
return x.WalletBalance
}
return 0
}
func (x *CreatePaymentResponse) GetInvoiceId() string {
if x != nil {
return x.InvoiceId
}
return ""
}
func (x *CreatePaymentResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type ListPaymentHistoryRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListPaymentHistoryRequest) Reset() {
*x = ListPaymentHistoryRequest{}
mi := &file_app_v1_payments_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListPaymentHistoryRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPaymentHistoryRequest) ProtoMessage() {}
func (x *ListPaymentHistoryRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListPaymentHistoryRequest.ProtoReflect.Descriptor instead.
func (*ListPaymentHistoryRequest) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{2}
}
type ListPaymentHistoryResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Payments []*PaymentHistoryItem `protobuf:"bytes,1,rep,name=payments,proto3" json:"payments,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListPaymentHistoryResponse) Reset() {
*x = ListPaymentHistoryResponse{}
mi := &file_app_v1_payments_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListPaymentHistoryResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPaymentHistoryResponse) ProtoMessage() {}
func (x *ListPaymentHistoryResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListPaymentHistoryResponse.ProtoReflect.Descriptor instead.
func (*ListPaymentHistoryResponse) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{3}
}
func (x *ListPaymentHistoryResponse) GetPayments() []*PaymentHistoryItem {
if x != nil {
return x.Payments
}
return nil
}
type TopupWalletRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Amount float64 `protobuf:"fixed64,1,opt,name=amount,proto3" json:"amount,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TopupWalletRequest) Reset() {
*x = TopupWalletRequest{}
mi := &file_app_v1_payments_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TopupWalletRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TopupWalletRequest) ProtoMessage() {}
func (x *TopupWalletRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TopupWalletRequest.ProtoReflect.Descriptor instead.
func (*TopupWalletRequest) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{4}
}
func (x *TopupWalletRequest) GetAmount() float64 {
if x != nil {
return x.Amount
}
return 0
}
type TopupWalletResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
WalletTransaction *WalletTransaction `protobuf:"bytes,1,opt,name=wallet_transaction,json=walletTransaction,proto3" json:"wallet_transaction,omitempty"`
WalletBalance float64 `protobuf:"fixed64,2,opt,name=wallet_balance,json=walletBalance,proto3" json:"wallet_balance,omitempty"`
InvoiceId string `protobuf:"bytes,3,opt,name=invoice_id,json=invoiceId,proto3" json:"invoice_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TopupWalletResponse) Reset() {
*x = TopupWalletResponse{}
mi := &file_app_v1_payments_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TopupWalletResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TopupWalletResponse) ProtoMessage() {}
func (x *TopupWalletResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TopupWalletResponse.ProtoReflect.Descriptor instead.
func (*TopupWalletResponse) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{5}
}
func (x *TopupWalletResponse) GetWalletTransaction() *WalletTransaction {
if x != nil {
return x.WalletTransaction
}
return nil
}
func (x *TopupWalletResponse) GetWalletBalance() float64 {
if x != nil {
return x.WalletBalance
}
return 0
}
func (x *TopupWalletResponse) GetInvoiceId() string {
if x != nil {
return x.InvoiceId
}
return ""
}
type DownloadInvoiceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DownloadInvoiceRequest) Reset() {
*x = DownloadInvoiceRequest{}
mi := &file_app_v1_payments_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DownloadInvoiceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DownloadInvoiceRequest) ProtoMessage() {}
func (x *DownloadInvoiceRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DownloadInvoiceRequest.ProtoReflect.Descriptor instead.
func (*DownloadInvoiceRequest) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{6}
}
func (x *DownloadInvoiceRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type DownloadInvoiceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"`
ContentType string `protobuf:"bytes,2,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DownloadInvoiceResponse) Reset() {
*x = DownloadInvoiceResponse{}
mi := &file_app_v1_payments_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DownloadInvoiceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DownloadInvoiceResponse) ProtoMessage() {}
func (x *DownloadInvoiceResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_payments_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DownloadInvoiceResponse.ProtoReflect.Descriptor instead.
func (*DownloadInvoiceResponse) Descriptor() ([]byte, []int) {
return file_app_v1_payments_proto_rawDescGZIP(), []int{7}
}
func (x *DownloadInvoiceResponse) GetFilename() string {
if x != nil {
return x.Filename
}
return ""
}
func (x *DownloadInvoiceResponse) GetContentType() string {
if x != nil {
return x.ContentType
}
return ""
}
func (x *DownloadInvoiceResponse) GetContent() string {
if x != nil {
return x.Content
}
return ""
}
var File_app_v1_payments_proto protoreflect.FileDescriptor
const file_app_v1_payments_proto_rawDesc = "" +
"\n" +
"\x15app/v1/payments.proto\x12\rstream.app.v1\x1a\x13app/v1/common.proto\"\xb0\x01\n" +
"\x14CreatePaymentRequest\x12\x17\n" +
"\aplan_id\x18\x01 \x01(\tR\x06planId\x12\x1f\n" +
"\vterm_months\x18\x02 \x01(\x05R\n" +
"termMonths\x12%\n" +
"\x0epayment_method\x18\x03 \x01(\tR\rpaymentMethod\x12&\n" +
"\ftopup_amount\x18\x04 \x01(\x01H\x00R\vtopupAmount\x88\x01\x01B\x0f\n" +
"\r_topup_amount\"\xee\x01\n" +
"\x15CreatePaymentResponse\x120\n" +
"\apayment\x18\x01 \x01(\v2\x16.stream.app.v1.PaymentR\apayment\x12C\n" +
"\fsubscription\x18\x02 \x01(\v2\x1f.stream.app.v1.PlanSubscriptionR\fsubscription\x12%\n" +
"\x0ewallet_balance\x18\x03 \x01(\x01R\rwalletBalance\x12\x1d\n" +
"\n" +
"invoice_id\x18\x04 \x01(\tR\tinvoiceId\x12\x18\n" +
"\amessage\x18\x05 \x01(\tR\amessage\"\x1b\n" +
"\x19ListPaymentHistoryRequest\"[\n" +
"\x1aListPaymentHistoryResponse\x12=\n" +
"\bpayments\x18\x01 \x03(\v2!.stream.app.v1.PaymentHistoryItemR\bpayments\",\n" +
"\x12TopupWalletRequest\x12\x16\n" +
"\x06amount\x18\x01 \x01(\x01R\x06amount\"\xac\x01\n" +
"\x13TopupWalletResponse\x12O\n" +
"\x12wallet_transaction\x18\x01 \x01(\v2 .stream.app.v1.WalletTransactionR\x11walletTransaction\x12%\n" +
"\x0ewallet_balance\x18\x02 \x01(\x01R\rwalletBalance\x12\x1d\n" +
"\n" +
"invoice_id\x18\x03 \x01(\tR\tinvoiceId\"(\n" +
"\x16DownloadInvoiceRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"r\n" +
"\x17DownloadInvoiceResponse\x12\x1a\n" +
"\bfilename\x18\x01 \x01(\tR\bfilename\x12!\n" +
"\fcontent_type\x18\x02 \x01(\tR\vcontentType\x12\x18\n" +
"\acontent\x18\x03 \x01(\tR\acontent2\x90\x03\n" +
"\x0fPaymentsService\x12Z\n" +
"\rCreatePayment\x12#.stream.app.v1.CreatePaymentRequest\x1a$.stream.app.v1.CreatePaymentResponse\x12i\n" +
"\x12ListPaymentHistory\x12(.stream.app.v1.ListPaymentHistoryRequest\x1a).stream.app.v1.ListPaymentHistoryResponse\x12T\n" +
"\vTopupWallet\x12!.stream.app.v1.TopupWalletRequest\x1a\".stream.app.v1.TopupWalletResponse\x12`\n" +
"\x0fDownloadInvoice\x12%.stream.app.v1.DownloadInvoiceRequest\x1a&.stream.app.v1.DownloadInvoiceResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
var (
file_app_v1_payments_proto_rawDescOnce sync.Once
file_app_v1_payments_proto_rawDescData []byte
)
func file_app_v1_payments_proto_rawDescGZIP() []byte {
file_app_v1_payments_proto_rawDescOnce.Do(func() {
file_app_v1_payments_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_v1_payments_proto_rawDesc), len(file_app_v1_payments_proto_rawDesc)))
})
return file_app_v1_payments_proto_rawDescData
}
var file_app_v1_payments_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_app_v1_payments_proto_goTypes = []any{
(*CreatePaymentRequest)(nil), // 0: stream.app.v1.CreatePaymentRequest
(*CreatePaymentResponse)(nil), // 1: stream.app.v1.CreatePaymentResponse
(*ListPaymentHistoryRequest)(nil), // 2: stream.app.v1.ListPaymentHistoryRequest
(*ListPaymentHistoryResponse)(nil), // 3: stream.app.v1.ListPaymentHistoryResponse
(*TopupWalletRequest)(nil), // 4: stream.app.v1.TopupWalletRequest
(*TopupWalletResponse)(nil), // 5: stream.app.v1.TopupWalletResponse
(*DownloadInvoiceRequest)(nil), // 6: stream.app.v1.DownloadInvoiceRequest
(*DownloadInvoiceResponse)(nil), // 7: stream.app.v1.DownloadInvoiceResponse
(*Payment)(nil), // 8: stream.app.v1.Payment
(*PlanSubscription)(nil), // 9: stream.app.v1.PlanSubscription
(*PaymentHistoryItem)(nil), // 10: stream.app.v1.PaymentHistoryItem
(*WalletTransaction)(nil), // 11: stream.app.v1.WalletTransaction
}
var file_app_v1_payments_proto_depIdxs = []int32{
8, // 0: stream.app.v1.CreatePaymentResponse.payment:type_name -> stream.app.v1.Payment
9, // 1: stream.app.v1.CreatePaymentResponse.subscription:type_name -> stream.app.v1.PlanSubscription
10, // 2: stream.app.v1.ListPaymentHistoryResponse.payments:type_name -> stream.app.v1.PaymentHistoryItem
11, // 3: stream.app.v1.TopupWalletResponse.wallet_transaction:type_name -> stream.app.v1.WalletTransaction
0, // 4: stream.app.v1.PaymentsService.CreatePayment:input_type -> stream.app.v1.CreatePaymentRequest
2, // 5: stream.app.v1.PaymentsService.ListPaymentHistory:input_type -> stream.app.v1.ListPaymentHistoryRequest
4, // 6: stream.app.v1.PaymentsService.TopupWallet:input_type -> stream.app.v1.TopupWalletRequest
6, // 7: stream.app.v1.PaymentsService.DownloadInvoice:input_type -> stream.app.v1.DownloadInvoiceRequest
1, // 8: stream.app.v1.PaymentsService.CreatePayment:output_type -> stream.app.v1.CreatePaymentResponse
3, // 9: stream.app.v1.PaymentsService.ListPaymentHistory:output_type -> stream.app.v1.ListPaymentHistoryResponse
5, // 10: stream.app.v1.PaymentsService.TopupWallet:output_type -> stream.app.v1.TopupWalletResponse
7, // 11: stream.app.v1.PaymentsService.DownloadInvoice:output_type -> stream.app.v1.DownloadInvoiceResponse
8, // [8:12] is the sub-list for method output_type
4, // [4:8] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_app_v1_payments_proto_init() }
func file_app_v1_payments_proto_init() {
if File_app_v1_payments_proto != nil {
return
}
file_app_v1_common_proto_init()
file_app_v1_payments_proto_msgTypes[0].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_payments_proto_rawDesc), len(file_app_v1_payments_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_app_v1_payments_proto_goTypes,
DependencyIndexes: file_app_v1_payments_proto_depIdxs,
MessageInfos: file_app_v1_payments_proto_msgTypes,
}.Build()
File_app_v1_payments_proto = out.File
file_app_v1_payments_proto_goTypes = nil
file_app_v1_payments_proto_depIdxs = nil
}

View File

@@ -0,0 +1,235 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: app/v1/payments.proto
package appv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
PaymentsService_CreatePayment_FullMethodName = "/stream.app.v1.PaymentsService/CreatePayment"
PaymentsService_ListPaymentHistory_FullMethodName = "/stream.app.v1.PaymentsService/ListPaymentHistory"
PaymentsService_TopupWallet_FullMethodName = "/stream.app.v1.PaymentsService/TopupWallet"
PaymentsService_DownloadInvoice_FullMethodName = "/stream.app.v1.PaymentsService/DownloadInvoice"
)
// PaymentsServiceClient is the client API for PaymentsService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type PaymentsServiceClient interface {
CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error)
ListPaymentHistory(ctx context.Context, in *ListPaymentHistoryRequest, opts ...grpc.CallOption) (*ListPaymentHistoryResponse, error)
TopupWallet(ctx context.Context, in *TopupWalletRequest, opts ...grpc.CallOption) (*TopupWalletResponse, error)
DownloadInvoice(ctx context.Context, in *DownloadInvoiceRequest, opts ...grpc.CallOption) (*DownloadInvoiceResponse, error)
}
type paymentsServiceClient struct {
cc grpc.ClientConnInterface
}
func NewPaymentsServiceClient(cc grpc.ClientConnInterface) PaymentsServiceClient {
return &paymentsServiceClient{cc}
}
func (c *paymentsServiceClient) CreatePayment(ctx context.Context, in *CreatePaymentRequest, opts ...grpc.CallOption) (*CreatePaymentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreatePaymentResponse)
err := c.cc.Invoke(ctx, PaymentsService_CreatePayment_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *paymentsServiceClient) ListPaymentHistory(ctx context.Context, in *ListPaymentHistoryRequest, opts ...grpc.CallOption) (*ListPaymentHistoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListPaymentHistoryResponse)
err := c.cc.Invoke(ctx, PaymentsService_ListPaymentHistory_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *paymentsServiceClient) TopupWallet(ctx context.Context, in *TopupWalletRequest, opts ...grpc.CallOption) (*TopupWalletResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(TopupWalletResponse)
err := c.cc.Invoke(ctx, PaymentsService_TopupWallet_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *paymentsServiceClient) DownloadInvoice(ctx context.Context, in *DownloadInvoiceRequest, opts ...grpc.CallOption) (*DownloadInvoiceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DownloadInvoiceResponse)
err := c.cc.Invoke(ctx, PaymentsService_DownloadInvoice_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// PaymentsServiceServer is the server API for PaymentsService service.
// All implementations must embed UnimplementedPaymentsServiceServer
// for forward compatibility.
type PaymentsServiceServer interface {
CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error)
ListPaymentHistory(context.Context, *ListPaymentHistoryRequest) (*ListPaymentHistoryResponse, error)
TopupWallet(context.Context, *TopupWalletRequest) (*TopupWalletResponse, error)
DownloadInvoice(context.Context, *DownloadInvoiceRequest) (*DownloadInvoiceResponse, error)
mustEmbedUnimplementedPaymentsServiceServer()
}
// UnimplementedPaymentsServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedPaymentsServiceServer struct{}
func (UnimplementedPaymentsServiceServer) CreatePayment(context.Context, *CreatePaymentRequest) (*CreatePaymentResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreatePayment not implemented")
}
func (UnimplementedPaymentsServiceServer) ListPaymentHistory(context.Context, *ListPaymentHistoryRequest) (*ListPaymentHistoryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListPaymentHistory not implemented")
}
func (UnimplementedPaymentsServiceServer) TopupWallet(context.Context, *TopupWalletRequest) (*TopupWalletResponse, error) {
return nil, status.Error(codes.Unimplemented, "method TopupWallet not implemented")
}
func (UnimplementedPaymentsServiceServer) DownloadInvoice(context.Context, *DownloadInvoiceRequest) (*DownloadInvoiceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DownloadInvoice not implemented")
}
func (UnimplementedPaymentsServiceServer) mustEmbedUnimplementedPaymentsServiceServer() {}
func (UnimplementedPaymentsServiceServer) testEmbeddedByValue() {}
// UnsafePaymentsServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to PaymentsServiceServer will
// result in compilation errors.
type UnsafePaymentsServiceServer interface {
mustEmbedUnimplementedPaymentsServiceServer()
}
func RegisterPaymentsServiceServer(s grpc.ServiceRegistrar, srv PaymentsServiceServer) {
// If the following call panics, it indicates UnimplementedPaymentsServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&PaymentsService_ServiceDesc, srv)
}
func _PaymentsService_CreatePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreatePaymentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PaymentsServiceServer).CreatePayment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PaymentsService_CreatePayment_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PaymentsServiceServer).CreatePayment(ctx, req.(*CreatePaymentRequest))
}
return interceptor(ctx, in, info, handler)
}
func _PaymentsService_ListPaymentHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPaymentHistoryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PaymentsServiceServer).ListPaymentHistory(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PaymentsService_ListPaymentHistory_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PaymentsServiceServer).ListPaymentHistory(ctx, req.(*ListPaymentHistoryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _PaymentsService_TopupWallet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TopupWalletRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PaymentsServiceServer).TopupWallet(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PaymentsService_TopupWallet_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PaymentsServiceServer).TopupWallet(ctx, req.(*TopupWalletRequest))
}
return interceptor(ctx, in, info, handler)
}
func _PaymentsService_DownloadInvoice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DownloadInvoiceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PaymentsServiceServer).DownloadInvoice(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PaymentsService_DownloadInvoice_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PaymentsServiceServer).DownloadInvoice(ctx, req.(*DownloadInvoiceRequest))
}
return interceptor(ctx, in, info, handler)
}
// PaymentsService_ServiceDesc is the grpc.ServiceDesc for PaymentsService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var PaymentsService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.PaymentsService",
HandlerType: (*PaymentsServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreatePayment",
Handler: _PaymentsService_CreatePayment_Handler,
},
{
MethodName: "ListPaymentHistory",
Handler: _PaymentsService_ListPaymentHistory_Handler,
},
{
MethodName: "TopupWallet",
Handler: _PaymentsService_TopupWallet_Handler,
},
{
MethodName: "DownloadInvoice",
Handler: _PaymentsService_DownloadInvoice_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/payments.proto",
}

View File

@@ -0,0 +1,810 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: app/v1/videos.proto
package appv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GetUploadUrlRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetUploadUrlRequest) Reset() {
*x = GetUploadUrlRequest{}
mi := &file_app_v1_videos_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetUploadUrlRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUploadUrlRequest) ProtoMessage() {}
func (x *GetUploadUrlRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUploadUrlRequest.ProtoReflect.Descriptor instead.
func (*GetUploadUrlRequest) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{0}
}
func (x *GetUploadUrlRequest) GetFilename() string {
if x != nil {
return x.Filename
}
return ""
}
type GetUploadUrlResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
UploadUrl string `protobuf:"bytes,1,opt,name=upload_url,json=uploadUrl,proto3" json:"upload_url,omitempty"`
Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
FileId string `protobuf:"bytes,3,opt,name=file_id,json=fileId,proto3" json:"file_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetUploadUrlResponse) Reset() {
*x = GetUploadUrlResponse{}
mi := &file_app_v1_videos_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetUploadUrlResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUploadUrlResponse) ProtoMessage() {}
func (x *GetUploadUrlResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUploadUrlResponse.ProtoReflect.Descriptor instead.
func (*GetUploadUrlResponse) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{1}
}
func (x *GetUploadUrlResponse) GetUploadUrl() string {
if x != nil {
return x.UploadUrl
}
return ""
}
func (x *GetUploadUrlResponse) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *GetUploadUrlResponse) GetFileId() string {
if x != nil {
return x.FileId
}
return ""
}
type CreateVideoRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
Description *string `protobuf:"bytes,2,opt,name=description,proto3,oneof" json:"description,omitempty"`
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
Duration int32 `protobuf:"varint,5,opt,name=duration,proto3" json:"duration,omitempty"`
Format *string `protobuf:"bytes,6,opt,name=format,proto3,oneof" json:"format,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateVideoRequest) Reset() {
*x = CreateVideoRequest{}
mi := &file_app_v1_videos_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateVideoRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateVideoRequest) ProtoMessage() {}
func (x *CreateVideoRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateVideoRequest.ProtoReflect.Descriptor instead.
func (*CreateVideoRequest) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{2}
}
func (x *CreateVideoRequest) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *CreateVideoRequest) GetDescription() string {
if x != nil && x.Description != nil {
return *x.Description
}
return ""
}
func (x *CreateVideoRequest) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *CreateVideoRequest) GetSize() int64 {
if x != nil {
return x.Size
}
return 0
}
func (x *CreateVideoRequest) GetDuration() int32 {
if x != nil {
return x.Duration
}
return 0
}
func (x *CreateVideoRequest) GetFormat() string {
if x != nil && x.Format != nil {
return *x.Format
}
return ""
}
type CreateVideoResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Video *Video `protobuf:"bytes,1,opt,name=video,proto3" json:"video,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateVideoResponse) Reset() {
*x = CreateVideoResponse{}
mi := &file_app_v1_videos_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateVideoResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateVideoResponse) ProtoMessage() {}
func (x *CreateVideoResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateVideoResponse.ProtoReflect.Descriptor instead.
func (*CreateVideoResponse) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{3}
}
func (x *CreateVideoResponse) GetVideo() *Video {
if x != nil {
return x.Video
}
return nil
}
type ListVideosRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"`
Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
Search *string `protobuf:"bytes,3,opt,name=search,proto3,oneof" json:"search,omitempty"`
Status *string `protobuf:"bytes,4,opt,name=status,proto3,oneof" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListVideosRequest) Reset() {
*x = ListVideosRequest{}
mi := &file_app_v1_videos_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListVideosRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListVideosRequest) ProtoMessage() {}
func (x *ListVideosRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListVideosRequest.ProtoReflect.Descriptor instead.
func (*ListVideosRequest) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{4}
}
func (x *ListVideosRequest) GetPage() int32 {
if x != nil {
return x.Page
}
return 0
}
func (x *ListVideosRequest) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
func (x *ListVideosRequest) GetSearch() string {
if x != nil && x.Search != nil {
return *x.Search
}
return ""
}
func (x *ListVideosRequest) GetStatus() string {
if x != nil && x.Status != nil {
return *x.Status
}
return ""
}
type ListVideosResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Videos []*Video `protobuf:"bytes,1,rep,name=videos,proto3" json:"videos,omitempty"`
Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"`
Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"`
Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListVideosResponse) Reset() {
*x = ListVideosResponse{}
mi := &file_app_v1_videos_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListVideosResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListVideosResponse) ProtoMessage() {}
func (x *ListVideosResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListVideosResponse.ProtoReflect.Descriptor instead.
func (*ListVideosResponse) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{5}
}
func (x *ListVideosResponse) GetVideos() []*Video {
if x != nil {
return x.Videos
}
return nil
}
func (x *ListVideosResponse) GetTotal() int64 {
if x != nil {
return x.Total
}
return 0
}
func (x *ListVideosResponse) GetPage() int32 {
if x != nil {
return x.Page
}
return 0
}
func (x *ListVideosResponse) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
type GetVideoRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetVideoRequest) Reset() {
*x = GetVideoRequest{}
mi := &file_app_v1_videos_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetVideoRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetVideoRequest) ProtoMessage() {}
func (x *GetVideoRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetVideoRequest.ProtoReflect.Descriptor instead.
func (*GetVideoRequest) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{6}
}
func (x *GetVideoRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type GetVideoResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Video *Video `protobuf:"bytes,1,opt,name=video,proto3" json:"video,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetVideoResponse) Reset() {
*x = GetVideoResponse{}
mi := &file_app_v1_videos_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetVideoResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetVideoResponse) ProtoMessage() {}
func (x *GetVideoResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetVideoResponse.ProtoReflect.Descriptor instead.
func (*GetVideoResponse) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{7}
}
func (x *GetVideoResponse) GetVideo() *Video {
if x != nil {
return x.Video
}
return nil
}
type UpdateVideoRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
Description *string `protobuf:"bytes,3,opt,name=description,proto3,oneof" json:"description,omitempty"`
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
Duration int32 `protobuf:"varint,6,opt,name=duration,proto3" json:"duration,omitempty"`
Format *string `protobuf:"bytes,7,opt,name=format,proto3,oneof" json:"format,omitempty"`
Status *string `protobuf:"bytes,8,opt,name=status,proto3,oneof" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateVideoRequest) Reset() {
*x = UpdateVideoRequest{}
mi := &file_app_v1_videos_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateVideoRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateVideoRequest) ProtoMessage() {}
func (x *UpdateVideoRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateVideoRequest.ProtoReflect.Descriptor instead.
func (*UpdateVideoRequest) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{8}
}
func (x *UpdateVideoRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *UpdateVideoRequest) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *UpdateVideoRequest) GetDescription() string {
if x != nil && x.Description != nil {
return *x.Description
}
return ""
}
func (x *UpdateVideoRequest) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *UpdateVideoRequest) GetSize() int64 {
if x != nil {
return x.Size
}
return 0
}
func (x *UpdateVideoRequest) GetDuration() int32 {
if x != nil {
return x.Duration
}
return 0
}
func (x *UpdateVideoRequest) GetFormat() string {
if x != nil && x.Format != nil {
return *x.Format
}
return ""
}
func (x *UpdateVideoRequest) GetStatus() string {
if x != nil && x.Status != nil {
return *x.Status
}
return ""
}
type UpdateVideoResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Video *Video `protobuf:"bytes,1,opt,name=video,proto3" json:"video,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateVideoResponse) Reset() {
*x = UpdateVideoResponse{}
mi := &file_app_v1_videos_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateVideoResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateVideoResponse) ProtoMessage() {}
func (x *UpdateVideoResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateVideoResponse.ProtoReflect.Descriptor instead.
func (*UpdateVideoResponse) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{9}
}
func (x *UpdateVideoResponse) GetVideo() *Video {
if x != nil {
return x.Video
}
return nil
}
type DeleteVideoRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteVideoRequest) Reset() {
*x = DeleteVideoRequest{}
mi := &file_app_v1_videos_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteVideoRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteVideoRequest) ProtoMessage() {}
func (x *DeleteVideoRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_videos_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteVideoRequest.ProtoReflect.Descriptor instead.
func (*DeleteVideoRequest) Descriptor() ([]byte, []int) {
return file_app_v1_videos_proto_rawDescGZIP(), []int{10}
}
func (x *DeleteVideoRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
var File_app_v1_videos_proto protoreflect.FileDescriptor
const file_app_v1_videos_proto_rawDesc = "" +
"\n" +
"\x13app/v1/videos.proto\x12\rstream.app.v1\x1a\x13app/v1/common.proto\"1\n" +
"\x13GetUploadUrlRequest\x12\x1a\n" +
"\bfilename\x18\x01 \x01(\tR\bfilename\"`\n" +
"\x14GetUploadUrlResponse\x12\x1d\n" +
"\n" +
"upload_url\x18\x01 \x01(\tR\tuploadUrl\x12\x10\n" +
"\x03key\x18\x02 \x01(\tR\x03key\x12\x17\n" +
"\afile_id\x18\x03 \x01(\tR\x06fileId\"\xcb\x01\n" +
"\x12CreateVideoRequest\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12%\n" +
"\vdescription\x18\x02 \x01(\tH\x00R\vdescription\x88\x01\x01\x12\x10\n" +
"\x03url\x18\x03 \x01(\tR\x03url\x12\x12\n" +
"\x04size\x18\x04 \x01(\x03R\x04size\x12\x1a\n" +
"\bduration\x18\x05 \x01(\x05R\bduration\x12\x1b\n" +
"\x06format\x18\x06 \x01(\tH\x01R\x06format\x88\x01\x01B\x0e\n" +
"\f_descriptionB\t\n" +
"\a_format\"A\n" +
"\x13CreateVideoResponse\x12*\n" +
"\x05video\x18\x01 \x01(\v2\x14.stream.app.v1.VideoR\x05video\"\x8d\x01\n" +
"\x11ListVideosRequest\x12\x12\n" +
"\x04page\x18\x01 \x01(\x05R\x04page\x12\x14\n" +
"\x05limit\x18\x02 \x01(\x05R\x05limit\x12\x1b\n" +
"\x06search\x18\x03 \x01(\tH\x00R\x06search\x88\x01\x01\x12\x1b\n" +
"\x06status\x18\x04 \x01(\tH\x01R\x06status\x88\x01\x01B\t\n" +
"\a_searchB\t\n" +
"\a_status\"\x82\x01\n" +
"\x12ListVideosResponse\x12,\n" +
"\x06videos\x18\x01 \x03(\v2\x14.stream.app.v1.VideoR\x06videos\x12\x14\n" +
"\x05total\x18\x02 \x01(\x03R\x05total\x12\x12\n" +
"\x04page\x18\x03 \x01(\x05R\x04page\x12\x14\n" +
"\x05limit\x18\x04 \x01(\x05R\x05limit\"!\n" +
"\x0fGetVideoRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\">\n" +
"\x10GetVideoResponse\x12*\n" +
"\x05video\x18\x01 \x01(\v2\x14.stream.app.v1.VideoR\x05video\"\x83\x02\n" +
"\x12UpdateVideoRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
"\x05title\x18\x02 \x01(\tR\x05title\x12%\n" +
"\vdescription\x18\x03 \x01(\tH\x00R\vdescription\x88\x01\x01\x12\x10\n" +
"\x03url\x18\x04 \x01(\tR\x03url\x12\x12\n" +
"\x04size\x18\x05 \x01(\x03R\x04size\x12\x1a\n" +
"\bduration\x18\x06 \x01(\x05R\bduration\x12\x1b\n" +
"\x06format\x18\a \x01(\tH\x01R\x06format\x88\x01\x01\x12\x1b\n" +
"\x06status\x18\b \x01(\tH\x02R\x06status\x88\x01\x01B\x0e\n" +
"\f_descriptionB\t\n" +
"\a_formatB\t\n" +
"\a_status\"A\n" +
"\x13UpdateVideoResponse\x12*\n" +
"\x05video\x18\x01 \x01(\v2\x14.stream.app.v1.VideoR\x05video\"$\n" +
"\x12DeleteVideoRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id2\x86\x04\n" +
"\rVideosService\x12W\n" +
"\fGetUploadUrl\x12\".stream.app.v1.GetUploadUrlRequest\x1a#.stream.app.v1.GetUploadUrlResponse\x12T\n" +
"\vCreateVideo\x12!.stream.app.v1.CreateVideoRequest\x1a\".stream.app.v1.CreateVideoResponse\x12Q\n" +
"\n" +
"ListVideos\x12 .stream.app.v1.ListVideosRequest\x1a!.stream.app.v1.ListVideosResponse\x12K\n" +
"\bGetVideo\x12\x1e.stream.app.v1.GetVideoRequest\x1a\x1f.stream.app.v1.GetVideoResponse\x12T\n" +
"\vUpdateVideo\x12!.stream.app.v1.UpdateVideoRequest\x1a\".stream.app.v1.UpdateVideoResponse\x12P\n" +
"\vDeleteVideo\x12!.stream.app.v1.DeleteVideoRequest\x1a\x1e.stream.app.v1.MessageResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
var (
file_app_v1_videos_proto_rawDescOnce sync.Once
file_app_v1_videos_proto_rawDescData []byte
)
func file_app_v1_videos_proto_rawDescGZIP() []byte {
file_app_v1_videos_proto_rawDescOnce.Do(func() {
file_app_v1_videos_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_v1_videos_proto_rawDesc), len(file_app_v1_videos_proto_rawDesc)))
})
return file_app_v1_videos_proto_rawDescData
}
var file_app_v1_videos_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_app_v1_videos_proto_goTypes = []any{
(*GetUploadUrlRequest)(nil), // 0: stream.app.v1.GetUploadUrlRequest
(*GetUploadUrlResponse)(nil), // 1: stream.app.v1.GetUploadUrlResponse
(*CreateVideoRequest)(nil), // 2: stream.app.v1.CreateVideoRequest
(*CreateVideoResponse)(nil), // 3: stream.app.v1.CreateVideoResponse
(*ListVideosRequest)(nil), // 4: stream.app.v1.ListVideosRequest
(*ListVideosResponse)(nil), // 5: stream.app.v1.ListVideosResponse
(*GetVideoRequest)(nil), // 6: stream.app.v1.GetVideoRequest
(*GetVideoResponse)(nil), // 7: stream.app.v1.GetVideoResponse
(*UpdateVideoRequest)(nil), // 8: stream.app.v1.UpdateVideoRequest
(*UpdateVideoResponse)(nil), // 9: stream.app.v1.UpdateVideoResponse
(*DeleteVideoRequest)(nil), // 10: stream.app.v1.DeleteVideoRequest
(*Video)(nil), // 11: stream.app.v1.Video
(*MessageResponse)(nil), // 12: stream.app.v1.MessageResponse
}
var file_app_v1_videos_proto_depIdxs = []int32{
11, // 0: stream.app.v1.CreateVideoResponse.video:type_name -> stream.app.v1.Video
11, // 1: stream.app.v1.ListVideosResponse.videos:type_name -> stream.app.v1.Video
11, // 2: stream.app.v1.GetVideoResponse.video:type_name -> stream.app.v1.Video
11, // 3: stream.app.v1.UpdateVideoResponse.video:type_name -> stream.app.v1.Video
0, // 4: stream.app.v1.VideosService.GetUploadUrl:input_type -> stream.app.v1.GetUploadUrlRequest
2, // 5: stream.app.v1.VideosService.CreateVideo:input_type -> stream.app.v1.CreateVideoRequest
4, // 6: stream.app.v1.VideosService.ListVideos:input_type -> stream.app.v1.ListVideosRequest
6, // 7: stream.app.v1.VideosService.GetVideo:input_type -> stream.app.v1.GetVideoRequest
8, // 8: stream.app.v1.VideosService.UpdateVideo:input_type -> stream.app.v1.UpdateVideoRequest
10, // 9: stream.app.v1.VideosService.DeleteVideo:input_type -> stream.app.v1.DeleteVideoRequest
1, // 10: stream.app.v1.VideosService.GetUploadUrl:output_type -> stream.app.v1.GetUploadUrlResponse
3, // 11: stream.app.v1.VideosService.CreateVideo:output_type -> stream.app.v1.CreateVideoResponse
5, // 12: stream.app.v1.VideosService.ListVideos:output_type -> stream.app.v1.ListVideosResponse
7, // 13: stream.app.v1.VideosService.GetVideo:output_type -> stream.app.v1.GetVideoResponse
9, // 14: stream.app.v1.VideosService.UpdateVideo:output_type -> stream.app.v1.UpdateVideoResponse
12, // 15: stream.app.v1.VideosService.DeleteVideo:output_type -> stream.app.v1.MessageResponse
10, // [10:16] is the sub-list for method output_type
4, // [4:10] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_app_v1_videos_proto_init() }
func file_app_v1_videos_proto_init() {
if File_app_v1_videos_proto != nil {
return
}
file_app_v1_common_proto_init()
file_app_v1_videos_proto_msgTypes[2].OneofWrappers = []any{}
file_app_v1_videos_proto_msgTypes[4].OneofWrappers = []any{}
file_app_v1_videos_proto_msgTypes[8].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_videos_proto_rawDesc), len(file_app_v1_videos_proto_rawDesc)),
NumEnums: 0,
NumMessages: 11,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_app_v1_videos_proto_goTypes,
DependencyIndexes: file_app_v1_videos_proto_depIdxs,
MessageInfos: file_app_v1_videos_proto_msgTypes,
}.Build()
File_app_v1_videos_proto = out.File
file_app_v1_videos_proto_goTypes = nil
file_app_v1_videos_proto_depIdxs = nil
}

View File

@@ -0,0 +1,311 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: app/v1/videos.proto
package appv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
VideosService_GetUploadUrl_FullMethodName = "/stream.app.v1.VideosService/GetUploadUrl"
VideosService_CreateVideo_FullMethodName = "/stream.app.v1.VideosService/CreateVideo"
VideosService_ListVideos_FullMethodName = "/stream.app.v1.VideosService/ListVideos"
VideosService_GetVideo_FullMethodName = "/stream.app.v1.VideosService/GetVideo"
VideosService_UpdateVideo_FullMethodName = "/stream.app.v1.VideosService/UpdateVideo"
VideosService_DeleteVideo_FullMethodName = "/stream.app.v1.VideosService/DeleteVideo"
)
// VideosServiceClient is the client API for VideosService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type VideosServiceClient interface {
GetUploadUrl(ctx context.Context, in *GetUploadUrlRequest, opts ...grpc.CallOption) (*GetUploadUrlResponse, error)
CreateVideo(ctx context.Context, in *CreateVideoRequest, opts ...grpc.CallOption) (*CreateVideoResponse, error)
ListVideos(ctx context.Context, in *ListVideosRequest, opts ...grpc.CallOption) (*ListVideosResponse, error)
GetVideo(ctx context.Context, in *GetVideoRequest, opts ...grpc.CallOption) (*GetVideoResponse, error)
UpdateVideo(ctx context.Context, in *UpdateVideoRequest, opts ...grpc.CallOption) (*UpdateVideoResponse, error)
DeleteVideo(ctx context.Context, in *DeleteVideoRequest, opts ...grpc.CallOption) (*MessageResponse, error)
}
type videosServiceClient struct {
cc grpc.ClientConnInterface
}
func NewVideosServiceClient(cc grpc.ClientConnInterface) VideosServiceClient {
return &videosServiceClient{cc}
}
func (c *videosServiceClient) GetUploadUrl(ctx context.Context, in *GetUploadUrlRequest, opts ...grpc.CallOption) (*GetUploadUrlResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetUploadUrlResponse)
err := c.cc.Invoke(ctx, VideosService_GetUploadUrl_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *videosServiceClient) CreateVideo(ctx context.Context, in *CreateVideoRequest, opts ...grpc.CallOption) (*CreateVideoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateVideoResponse)
err := c.cc.Invoke(ctx, VideosService_CreateVideo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *videosServiceClient) ListVideos(ctx context.Context, in *ListVideosRequest, opts ...grpc.CallOption) (*ListVideosResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListVideosResponse)
err := c.cc.Invoke(ctx, VideosService_ListVideos_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *videosServiceClient) GetVideo(ctx context.Context, in *GetVideoRequest, opts ...grpc.CallOption) (*GetVideoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetVideoResponse)
err := c.cc.Invoke(ctx, VideosService_GetVideo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *videosServiceClient) UpdateVideo(ctx context.Context, in *UpdateVideoRequest, opts ...grpc.CallOption) (*UpdateVideoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateVideoResponse)
err := c.cc.Invoke(ctx, VideosService_UpdateVideo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *videosServiceClient) DeleteVideo(ctx context.Context, in *DeleteVideoRequest, opts ...grpc.CallOption) (*MessageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MessageResponse)
err := c.cc.Invoke(ctx, VideosService_DeleteVideo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// VideosServiceServer is the server API for VideosService service.
// All implementations must embed UnimplementedVideosServiceServer
// for forward compatibility.
type VideosServiceServer interface {
GetUploadUrl(context.Context, *GetUploadUrlRequest) (*GetUploadUrlResponse, error)
CreateVideo(context.Context, *CreateVideoRequest) (*CreateVideoResponse, error)
ListVideos(context.Context, *ListVideosRequest) (*ListVideosResponse, error)
GetVideo(context.Context, *GetVideoRequest) (*GetVideoResponse, error)
UpdateVideo(context.Context, *UpdateVideoRequest) (*UpdateVideoResponse, error)
DeleteVideo(context.Context, *DeleteVideoRequest) (*MessageResponse, error)
mustEmbedUnimplementedVideosServiceServer()
}
// UnimplementedVideosServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedVideosServiceServer struct{}
func (UnimplementedVideosServiceServer) GetUploadUrl(context.Context, *GetUploadUrlRequest) (*GetUploadUrlResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUploadUrl not implemented")
}
func (UnimplementedVideosServiceServer) CreateVideo(context.Context, *CreateVideoRequest) (*CreateVideoResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateVideo not implemented")
}
func (UnimplementedVideosServiceServer) ListVideos(context.Context, *ListVideosRequest) (*ListVideosResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListVideos not implemented")
}
func (UnimplementedVideosServiceServer) GetVideo(context.Context, *GetVideoRequest) (*GetVideoResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetVideo not implemented")
}
func (UnimplementedVideosServiceServer) UpdateVideo(context.Context, *UpdateVideoRequest) (*UpdateVideoResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateVideo not implemented")
}
func (UnimplementedVideosServiceServer) DeleteVideo(context.Context, *DeleteVideoRequest) (*MessageResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteVideo not implemented")
}
func (UnimplementedVideosServiceServer) mustEmbedUnimplementedVideosServiceServer() {}
func (UnimplementedVideosServiceServer) testEmbeddedByValue() {}
// UnsafeVideosServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to VideosServiceServer will
// result in compilation errors.
type UnsafeVideosServiceServer interface {
mustEmbedUnimplementedVideosServiceServer()
}
func RegisterVideosServiceServer(s grpc.ServiceRegistrar, srv VideosServiceServer) {
// If the following call panics, it indicates UnimplementedVideosServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&VideosService_ServiceDesc, srv)
}
func _VideosService_GetUploadUrl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUploadUrlRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VideosServiceServer).GetUploadUrl(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: VideosService_GetUploadUrl_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VideosServiceServer).GetUploadUrl(ctx, req.(*GetUploadUrlRequest))
}
return interceptor(ctx, in, info, handler)
}
func _VideosService_CreateVideo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateVideoRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VideosServiceServer).CreateVideo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: VideosService_CreateVideo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VideosServiceServer).CreateVideo(ctx, req.(*CreateVideoRequest))
}
return interceptor(ctx, in, info, handler)
}
func _VideosService_ListVideos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListVideosRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VideosServiceServer).ListVideos(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: VideosService_ListVideos_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VideosServiceServer).ListVideos(ctx, req.(*ListVideosRequest))
}
return interceptor(ctx, in, info, handler)
}
func _VideosService_GetVideo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetVideoRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VideosServiceServer).GetVideo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: VideosService_GetVideo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VideosServiceServer).GetVideo(ctx, req.(*GetVideoRequest))
}
return interceptor(ctx, in, info, handler)
}
func _VideosService_UpdateVideo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateVideoRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VideosServiceServer).UpdateVideo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: VideosService_UpdateVideo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VideosServiceServer).UpdateVideo(ctx, req.(*UpdateVideoRequest))
}
return interceptor(ctx, in, info, handler)
}
func _VideosService_DeleteVideo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteVideoRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VideosServiceServer).DeleteVideo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: VideosService_DeleteVideo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VideosServiceServer).DeleteVideo(ctx, req.(*DeleteVideoRequest))
}
return interceptor(ctx, in, info, handler)
}
// VideosService_ServiceDesc is the grpc.ServiceDesc for VideosService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var VideosService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "stream.app.v1.VideosService",
HandlerType: (*VideosServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetUploadUrl",
Handler: _VideosService_GetUploadUrl_Handler,
},
{
MethodName: "CreateVideo",
Handler: _VideosService_CreateVideo_Handler,
},
{
MethodName: "ListVideos",
Handler: _VideosService_ListVideos_Handler,
},
{
MethodName: "GetVideo",
Handler: _VideosService_GetVideo_Handler,
},
{
MethodName: "UpdateVideo",
Handler: _VideosService_UpdateVideo_Handler,
},
{
MethodName: "DeleteVideo",
Handler: _VideosService_DeleteVideo_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "app/v1/videos.proto",
}

View File

@@ -0,0 +1,38 @@
//go:build ignore
// +build ignore
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"stream.api/internal/database/model"
"stream.api/pkg/response"
)
// RequireAdmin returns a Gin middleware that blocks non-admin users.
// Must be placed after the auth middleware so "user" is set in the context.
func RequireAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
userValue, exists := c.Get("user")
if !exists {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
user, ok := userValue.(*model.User)
if !ok || user == nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if user.Role == nil || strings.ToUpper(*user.Role) != "ADMIN" {
response.Error(c, http.StatusForbidden, "Admin access required")
return
}
c.Next()
}
}

View File

@@ -1,155 +1,52 @@
//go:build ignore
// +build ignore
package middleware package middleware
import ( import (
"net/http" "net/http"
"strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"stream.api/internal/config" "stream.api/internal/config"
"stream.api/internal/database/query"
"stream.api/pkg/cache" "stream.api/pkg/cache"
"stream.api/pkg/logger"
"stream.api/pkg/response" "stream.api/pkg/response"
"stream.api/pkg/token" "stream.api/pkg/token"
) )
const (
CookieName = "auth_token" // Backward compatibility if needed, but we use access_token now
)
type AuthMiddleware struct { type AuthMiddleware struct {
cache cache.Cache authenticator *Authenticator
token token.Provider
} }
func NewAuthMiddleware(c cache.Cache, t token.Provider, cfg *config.Config) *AuthMiddleware { func NewAuthMiddleware(c cache.Cache, t token.Provider, _ *config.Config, db *gorm.DB, l logger.Logger) *AuthMiddleware {
return &AuthMiddleware{ return &AuthMiddleware{
cache: c, authenticator: NewAuthenticator(c, t, db, l),
token: t,
} }
} }
func (m *AuthMiddleware) Handle() gin.HandlerFunc { func (m *AuthMiddleware) Handle() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
ctx := c.Request.Context() result, err := m.authenticator.Authenticate(c.Request.Context(), AuthRequest{
var userID string Authorization: c.GetHeader("Authorization"),
var claims *token.Claims CookieHeader: c.GetHeader("Cookie"),
var err error })
// 1. Try Access Token (Header or Cookie)
var accessToken string
authHeader := c.GetHeader("Authorization")
if len(authHeader) > 7 && strings.ToUpper(authHeader[0:7]) == "BEARER " {
accessToken = authHeader[7:]
}
if accessToken == "" {
cookie, err := c.Cookie("access_token")
if err == nil {
accessToken = cookie
}
}
if accessToken != "" {
claims, err = m.token.ParseToken(accessToken)
if err == nil {
userID = claims.UserID
}
}
// 2. If Access Token invalid/missing, Try Refresh Token
if userID == "" {
refreshToken, errRefresh := c.Cookie("refresh_token")
if errRefresh != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized: No token")
return
}
// Validate Refresh Token (JWT signature)
// Problem regarding pkg/token parsing: Generates MapClaims but ParseToken expects Claims struct.
// Let's rely on Redis mostly, or assume ParseToken *fails* if claims mismatch, which is fine.
// Better: Assume we can parse at least standard claims or check Redis directly?
// But we need the UUID inside the token to check Redis key `refresh_uuid:{uuid}`.
// WORKAROUND: In pkg/token/jwt.go we defined `rtClaims["refresh_uuid"]`.
// `ParseToken` attempts to map to `Claims`. It likely won't map unknown fields but `Valid` might be true if struct has RegisteredClaims?
// Actually `jwt.ParseWithClaims` will succeed if signature matches, even if some fields are missing in struct.
// But we need `refresh_uuid`!
//
// We MUST parse as MapClaims here or update pkg/token.
// For this execution, I will parse as MapClaims locally to unblock.
// Validate Refresh Token (JWT signature)
// Parse using local helper to check refresh_uuid claim
// We can use m.token.ParseMapToken which we implemented
rtClaims, err := m.token.ParseMapToken(refreshToken)
if err != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized: Invalid refresh token")
return
}
refreshUUID, ok := rtClaims["refresh_uuid"].(string)
if !ok {
response.Error(c, http.StatusUnauthorized, "Unauthorized: Invalid refresh token claim")
return
}
// Check Redis
redisKey := "refresh_uuid:" + refreshUUID
userIDStr, err := m.cache.Get(ctx, redisKey)
if err != nil {
// Assuming Get returns error on miss or failure
response.Error(c, http.StatusUnauthorized, "Unauthorized: Session expired or invalid")
return
}
userID = userIDStr
// Refresh Success: Generate NEW Pair (Rotational or just new Access?)
// Let's just issue new Access Token. Refresh Token rotation is safer but complex (need to update Redis key).
// We will just issue new Access Token.
// Need User Role/Email for Access Token claims.
u := query.User
userObj, err := u.WithContext(ctx).Where(u.ID.Eq(userID)).First()
if err != nil {
response.Error(c, http.StatusUnauthorized, "User not found")
return
}
// Calling pkg/token.GenerateTokenPair generates BOTH.
// If we want new Access we can regenerate pair and update tokens.
// Updating refresh token extends session (Slide expiration).
newTd, err := m.token.GenerateTokenPair(userID, userObj.Email, *userObj.Role)
if err == nil {
// Delete old refresh token from Redis?
m.cache.Del(ctx, redisKey)
// Set new
m.cache.Set(ctx, "refresh_uuid:"+newTd.RefreshUUID, userID, time.Until(time.Unix(newTd.RtExpires, 0)))
// Set Cookies
c.SetCookie("access_token", newTd.AccessToken, int(newTd.AtExpires-time.Now().Unix()), "/", "", false, true)
c.SetCookie("refresh_token", newTd.RefreshToken, int(newTd.RtExpires-time.Now().Unix()), "/", "", false, true)
}
}
// 3. User Lookup / Block Check
u := query.User
user, err := u.WithContext(ctx).Where(u.ID.Eq(userID)).First()
if err != nil { if err != nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized: User not found") authErr, ok := err.(*AuthError)
if !ok {
response.Error(c, http.StatusInternalServerError, "Authentication failed")
return
}
response.Error(c, authErr.StatusCode, authErr.Message)
return return
} }
if user.Role != nil && strings.ToLower(*user.Role) == "block" { for _, cookie := range result.SetCookies {
response.Error(c, http.StatusForbidden, "Forbidden: User is blocked") c.Header("Set-Cookie", cookie)
return
} }
c.Set("userID", user.ID) c.Set("userID", result.UserID)
c.Set("user", user) c.Set("user", result.User)
c.Next() c.Next()
} }
} }
// Helper to parse generic claims
// Removed parseMapToken as it is now in TokenProvider interface

View File

@@ -0,0 +1,247 @@
package middleware
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
"stream.api/pkg/logger"
)
const (
ActorMarkerMetadataKey = "x-stream-internal-auth"
ActorIDMetadataKey = "x-stream-actor-id"
ActorEmailMetadataKey = "x-stream-actor-email"
ActorRoleMetadataKey = "x-stream-actor-role"
)
type AuthResult struct {
UserID string
User *model.User
}
type Actor struct {
UserID string
Email string
Role string
}
type Authenticator struct {
db *gorm.DB
logger logger.Logger
trustedMarker string
}
func NewAuthenticator(db *gorm.DB, l logger.Logger, trustedMarker string) *Authenticator {
return &Authenticator{
db: db,
logger: l,
trustedMarker: strings.TrimSpace(trustedMarker),
}
}
func (a *Authenticator) Authenticate(ctx context.Context) (*AuthResult, error) {
actor, err := a.RequireActor(ctx)
if err != nil {
return nil, err
}
u := query.User
user, err := u.WithContext(ctx).Where(u.ID.Eq(actor.UserID)).First()
if err != nil {
return nil, status.Error(codes.Unauthenticated, "Unauthorized")
}
user, err = a.syncSubscriptionState(ctx, user)
if err != nil {
a.logger.Error("Failed to sync subscription state", "error", err, "user_id", actor.UserID)
return nil, status.Error(codes.Internal, "Failed to load user subscription state")
}
if user.Role != nil && strings.EqualFold(strings.TrimSpace(*user.Role), "block") {
return nil, status.Error(codes.PermissionDenied, "Forbidden: User is blocked")
}
return &AuthResult{
UserID: user.ID,
User: user,
}, nil
}
func (a *Authenticator) RequireActor(ctx context.Context) (*Actor, error) {
md, err := a.requireTrustedMetadata(ctx)
if err != nil {
return nil, err
}
userID := strings.TrimSpace(firstMetadataValue(md, ActorIDMetadataKey))
role := strings.TrimSpace(firstMetadataValue(md, ActorRoleMetadataKey))
if userID == "" || role == "" {
return nil, status.Error(codes.Unauthenticated, "Missing actor identity")
}
return &Actor{
UserID: userID,
Email: strings.TrimSpace(firstMetadataValue(md, ActorEmailMetadataKey)),
Role: role,
}, nil
}
func (a *Authenticator) RequireInternalCall(ctx context.Context) error {
_, err := a.requireTrustedMetadata(ctx)
return err
}
func (a *Authenticator) requireTrustedMetadata(ctx context.Context) (metadata.MD, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "Missing actor metadata")
}
marker := firstMetadataValue(md, ActorMarkerMetadataKey)
if marker == "" || marker != a.trustedMarker {
return nil, status.Error(codes.Unauthenticated, "Invalid internal auth marker")
}
return md, nil
}
func firstMetadataValue(md metadata.MD, key string) string {
values := md.Get(key)
if len(values) == 0 {
return ""
}
return values[0]
}
func (a *Authenticator) syncSubscriptionState(ctx context.Context, user *model.User) (*model.User, error) {
subscription, err := model.GetLatestPlanSubscription(ctx, a.db, user.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return user, nil
}
return nil, err
}
now := time.Now().UTC()
if err := a.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var lockedSubscription model.PlanSubscription
if err := tx.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", subscription.ID).
First(&lockedSubscription).Error; err != nil {
return err
}
if lockedSubscription.ExpiresAt.After(now) {
if user.PlanID == nil || strings.TrimSpace(*user.PlanID) != lockedSubscription.PlanID {
if err := tx.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", user.ID).
Update("plan_id", lockedSubscription.PlanID).Error; err != nil {
return err
}
user.PlanID = &lockedSubscription.PlanID
}
reminderDays, reminderField := reminderFieldForSubscription(&lockedSubscription, now)
if reminderField != "" {
sentAt := now
notification := &model.Notification{
ID: uuidString(),
UserID: user.ID,
Type: "billing.subscription_expiring",
Title: "Plan expiring soon",
Message: reminderMessage(reminderDays),
ActionURL: model.StringPtr("/settings/billing"),
ActionLabel: model.StringPtr("Renew plan"),
Metadata: model.StringPtr(mustMarshalAuthJSON(map[string]interface{}{"plan_id": lockedSubscription.PlanID, "expires_at": lockedSubscription.ExpiresAt.UTC().Format(time.RFC3339), "reminder_days": reminderDays})),
}
if err := tx.WithContext(ctx).Create(notification).Error; err != nil {
return err
}
if err := tx.WithContext(ctx).
Model(&model.PlanSubscription{}).
Where("id = ?", lockedSubscription.ID).
Update(reminderField, sentAt).Error; err != nil {
return err
}
}
return nil
}
if user.PlanID != nil && strings.TrimSpace(*user.PlanID) != "" {
if err := tx.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", user.ID).
Update("plan_id", nil).Error; err != nil {
return err
}
user.PlanID = nil
}
return nil
}); err != nil {
return nil, err
}
return user, nil
}
func reminderFieldForSubscription(subscription *model.PlanSubscription, now time.Time) (int, string) {
if subscription == nil || !subscription.ExpiresAt.After(now) {
return 0, ""
}
remaining := subscription.ExpiresAt.Sub(now)
switch {
case remaining <= 24*time.Hour:
if subscription.Reminder1DSentAt == nil {
return 1, "reminder_1d_sent_at"
}
case remaining <= 72*time.Hour:
if subscription.Reminder3DSentAt == nil {
return 3, "reminder_3d_sent_at"
}
case remaining <= 7*24*time.Hour:
if subscription.Reminder7DSentAt == nil {
return 7, "reminder_7d_sent_at"
}
}
return 0, ""
}
func reminderMessage(days int) string {
switch days {
case 1:
return "Your current plan expires in 1 day. Renew now to avoid interruption."
case 3:
return "Your current plan expires in 3 days. Renew now to keep access active."
default:
return "Your current plan expires in 7 days. Renew now to keep your plan active."
}
}
func mustMarshalAuthJSON(value interface{}) string {
encoded, err := json.Marshal(value)
if err != nil {
return "{}"
}
return string(encoded)
}
func uuidString() string {
return uuid.New().String()
}

View File

@@ -1,3 +1,6 @@
//go:build ignore
// +build ignore
package middleware package middleware
import ( import (

View File

@@ -0,0 +1,20 @@
package app
import (
"google.golang.org/grpc"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func Register(server grpc.ServiceRegistrar, services *Services) {
appv1.RegisterAuthServiceServer(server, services.AuthServiceServer)
appv1.RegisterAccountServiceServer(server, services.AccountServiceServer)
appv1.RegisterPreferencesServiceServer(server, services.PreferencesServiceServer)
appv1.RegisterUsageServiceServer(server, services.UsageServiceServer)
appv1.RegisterNotificationsServiceServer(server, services.NotificationsServiceServer)
appv1.RegisterDomainsServiceServer(server, services.DomainsServiceServer)
appv1.RegisterAdTemplatesServiceServer(server, services.AdTemplatesServiceServer)
appv1.RegisterPlansServiceServer(server, services.PlansServiceServer)
appv1.RegisterPaymentsServiceServer(server, services.PaymentsServiceServer)
appv1.RegisterVideosServiceServer(server, services.VideosServiceServer)
appv1.RegisterAdminServiceServer(server, services.AdminServiceServer)
}

View File

@@ -0,0 +1,185 @@
package app
import (
"context"
"errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
authapi "stream.api/internal/api/auth"
preferencesapi "stream.api/internal/api/preferences"
usageapi "stream.api/internal/api/usage"
"stream.api/internal/database/model"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func (s *appServices) GetMe(ctx context.Context, _ *appv1.GetMeRequest) (*appv1.GetMeResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
payload, err := authapi.BuildUserPayload(ctx, s.db, result.User)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to build user payload")
}
return &appv1.GetMeResponse{User: toProtoUserPayload(payload)}, nil
}
func (s *appServices) UpdateMe(ctx context.Context, req *appv1.UpdateMeRequest) (*appv1.UpdateMeResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
updatedUser, err := authapi.UpdateUserProfile(ctx, s.db, s.logger, result.UserID, authapi.UpdateProfileInput{
Username: req.Username,
Email: req.Email,
Language: req.Language,
Locale: req.Locale,
})
if err != nil {
switch {
case errors.Is(err, authapi.ErrEmailRequired), errors.Is(err, authapi.ErrEmailAlreadyRegistered):
return nil, status.Error(codes.InvalidArgument, err.Error())
default:
return nil, status.Error(codes.Internal, "Failed to update profile")
}
}
payload, err := authapi.BuildUserPayload(ctx, s.db, updatedUser)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to build user payload")
}
return &appv1.UpdateMeResponse{User: toProtoUser(payload)}, nil
}
func (s *appServices) DeleteMe(ctx context.Context, _ *appv1.DeleteMeRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
userID := result.UserID
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&model.Notification{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Domain{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.AdTemplate{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.WalletTransaction{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.PlanSubscription{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.UserPreference{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Payment{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Video{}).Error; err != nil {
return err
}
if err := tx.Where("id = ?", userID).Delete(&model.User{}).Error; err != nil {
return err
}
return nil
}); err != nil {
s.logger.Error("Failed to delete user", "error", err)
return nil, status.Error(codes.Internal, "Failed to delete account")
}
return messageResponse("Account deleted successfully"), nil
}
func (s *appServices) ClearMyData(ctx context.Context, _ *appv1.ClearMyDataRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
userID := result.UserID
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&model.Notification{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Domain{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.AdTemplate{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", userID).Delete(&model.Video{}).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{"storage_used": 0}).Error; err != nil {
return err
}
return nil
}); err != nil {
s.logger.Error("Failed to clear user data", "error", err)
return nil, status.Error(codes.Internal, "Failed to clear data")
}
return messageResponse("Data cleared successfully"), nil
}
func (s *appServices) GetPreferences(ctx context.Context, _ *appv1.GetPreferencesRequest) (*appv1.GetPreferencesResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
pref, err := preferencesapi.LoadUserPreferences(ctx, s.db, result.UserID)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to load preferences")
}
return &appv1.GetPreferencesResponse{Preferences: toProtoPreferences(pref)}, nil
}
func (s *appServices) UpdatePreferences(ctx context.Context, req *appv1.UpdatePreferencesRequest) (*appv1.UpdatePreferencesResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
pref, err := preferencesapi.UpdateUserPreferences(ctx, s.db, s.logger, result.UserID, preferencesapi.UpdateInput{
EmailNotifications: req.EmailNotifications,
PushNotifications: req.PushNotifications,
MarketingNotifications: req.MarketingNotifications,
TelegramNotifications: req.TelegramNotifications,
Autoplay: req.Autoplay,
Loop: req.Loop,
Muted: req.Muted,
ShowControls: req.ShowControls,
Pip: req.Pip,
Airplay: req.Airplay,
Chromecast: req.Chromecast,
Language: req.Language,
Locale: req.Locale,
})
if err != nil {
return nil, status.Error(codes.Internal, "Failed to save preferences")
}
return &appv1.UpdatePreferencesResponse{Preferences: toProtoPreferences(pref)}, nil
}
func (s *appServices) GetUsage(ctx context.Context, _ *appv1.GetUsageRequest) (*appv1.GetUsageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
payload, err := usageapi.LoadUsage(ctx, s.db, s.logger, result.User)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to load usage")
}
return &appv1.GetUsageResponse{
UserId: payload.UserID,
TotalVideos: payload.TotalVideos,
TotalStorage: payload.TotalStorage,
}, nil
}

View File

@@ -0,0 +1,672 @@
package app
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"stream.api/internal/database/model"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func (s *appServices) ListAdminPayments(ctx context.Context, req *appv1.ListAdminPaymentsRequest) (*appv1.ListAdminPaymentsResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
limitInt := int(limit)
userID := strings.TrimSpace(req.GetUserId())
statusFilter := strings.TrimSpace(req.GetStatus())
db := s.db.WithContext(ctx).Model(&model.Payment{})
if userID != "" {
db = db.Where("user_id = ?", userID)
}
if statusFilter != "" {
db = db.Where("UPPER(status) = ?", strings.ToUpper(statusFilter))
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list payments")
}
var payments []model.Payment
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&payments).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list payments")
}
items := make([]*appv1.AdminPayment, 0, len(payments))
for _, payment := range payments {
payload, err := s.buildAdminPayment(ctx, &payment)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list payments")
}
items = append(items, payload)
}
return &appv1.ListAdminPaymentsResponse{Payments: items, Total: total, Page: page, Limit: limit}, nil
}
func (s *appServices) GetAdminPayment(ctx context.Context, req *appv1.GetAdminPaymentRequest) (*appv1.GetAdminPaymentResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Payment not found")
}
var payment model.Payment
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&payment).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Payment not found")
}
return nil, status.Error(codes.Internal, "Failed to get payment")
}
payload, err := s.buildAdminPayment(ctx, &payment)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to get payment")
}
return &appv1.GetAdminPaymentResponse{Payment: payload}, nil
}
func (s *appServices) CreateAdminPayment(ctx context.Context, req *appv1.CreateAdminPaymentRequest) (*appv1.CreateAdminPaymentResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
userID := strings.TrimSpace(req.GetUserId())
planID := strings.TrimSpace(req.GetPlanId())
if userID == "" || planID == "" {
return nil, status.Error(codes.InvalidArgument, "User ID and plan ID are required")
}
if !isAllowedTermMonths(req.GetTermMonths()) {
return nil, status.Error(codes.InvalidArgument, "Term months must be one of 1, 3, 6, or 12")
}
paymentMethod := normalizePaymentMethod(req.GetPaymentMethod())
if paymentMethod == "" {
return nil, status.Error(codes.InvalidArgument, "Payment method must be wallet or topup")
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to create payment")
}
var planRecord model.Plan
if err := s.db.WithContext(ctx).Where("id = ?", planID).First(&planRecord).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "Plan not found")
}
return nil, status.Error(codes.Internal, "Failed to create payment")
}
if planRecord.IsActive == nil || !*planRecord.IsActive {
return nil, status.Error(codes.InvalidArgument, "Plan is not active")
}
totalAmount := planRecord.Price * float64(req.GetTermMonths())
statusValue := "SUCCESS"
provider := "INTERNAL"
currency := normalizeCurrency(nil)
transactionID := buildTransactionID("sub")
now := time.Now().UTC()
paymentRecord := &model.Payment{
ID: uuid.New().String(),
UserID: user.ID,
PlanID: &planRecord.ID,
Amount: totalAmount,
Currency: &currency,
Status: &statusValue,
Provider: &provider,
TransactionID: &transactionID,
}
invoiceID := buildInvoiceID(paymentRecord.ID)
var subscription *model.PlanSubscription
var walletBalance float64
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if _, err := lockUserForUpdate(ctx, tx, user.ID); err != nil {
return err
}
currentSubscription, err := model.GetLatestPlanSubscription(ctx, tx, user.ID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
baseExpiry := now
if currentSubscription != nil && currentSubscription.ExpiresAt.After(baseExpiry) {
baseExpiry = currentSubscription.ExpiresAt.UTC()
}
newExpiry := baseExpiry.AddDate(0, int(req.GetTermMonths()), 0)
currentWalletBalance, err := model.GetWalletBalance(ctx, tx, user.ID)
if err != nil {
return err
}
shortfall := maxFloat(totalAmount-currentWalletBalance, 0)
if paymentMethod == paymentMethodWallet && shortfall > 0 {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Insufficient wallet balance", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
})
}
topupAmount := 0.0
if paymentMethod == paymentMethodTopup {
if req.TopupAmount == nil {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount is required when payment method is topup", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
})
}
topupAmount = maxFloat(req.GetTopupAmount(), 0)
if topupAmount <= 0 {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount must be greater than 0", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
})
}
if topupAmount < shortfall {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount must be greater than or equal to the required shortfall", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
"topup_amount": topupAmount,
})
}
}
if err := tx.Create(paymentRecord).Error; err != nil {
return err
}
walletUsedAmount := totalAmount
if paymentMethod == paymentMethodTopup {
topupTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: user.ID,
Type: walletTransactionTypeTopup,
Amount: maxFloat(req.GetTopupAmount(), 0),
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Wallet top-up for %s (%d months)", planRecord.Name, req.GetTermMonths())),
PaymentID: &paymentRecord.ID,
PlanID: &planRecord.ID,
TermMonths: int32Ptr(req.GetTermMonths()),
}
if err := tx.Create(topupTransaction).Error; err != nil {
return err
}
}
debitTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: user.ID,
Type: walletTransactionTypeSubscriptionDebit,
Amount: -totalAmount,
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Subscription payment for %s (%d months)", planRecord.Name, req.GetTermMonths())),
PaymentID: &paymentRecord.ID,
PlanID: &planRecord.ID,
TermMonths: int32Ptr(req.GetTermMonths()),
}
if err := tx.Create(debitTransaction).Error; err != nil {
return err
}
subscription = &model.PlanSubscription{
ID: uuid.New().String(),
UserID: user.ID,
PaymentID: paymentRecord.ID,
PlanID: planRecord.ID,
TermMonths: req.GetTermMonths(),
PaymentMethod: paymentMethod,
WalletAmount: walletUsedAmount,
TopupAmount: maxFloat(req.GetTopupAmount(), 0),
StartedAt: now,
ExpiresAt: newExpiry,
}
if err := tx.Create(subscription).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Update("plan_id", planRecord.ID).Error; err != nil {
return err
}
notification := buildSubscriptionNotification(user.ID, paymentRecord.ID, invoiceID, &planRecord, subscription)
if err := tx.Create(notification).Error; err != nil {
return err
}
walletBalance, err = model.GetWalletBalance(ctx, tx, user.ID)
if err != nil {
return err
}
return nil
})
if err != nil {
if _, ok := status.FromError(err); ok {
return nil, err
}
return nil, status.Error(codes.Internal, "Failed to create payment")
}
payload, err := s.buildAdminPayment(ctx, paymentRecord)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create payment")
}
return &appv1.CreateAdminPaymentResponse{
Payment: payload,
Subscription: toProtoPlanSubscription(subscription),
WalletBalance: walletBalance,
InvoiceId: invoiceID,
}, nil
}
func (s *appServices) UpdateAdminPayment(ctx context.Context, req *appv1.UpdateAdminPaymentRequest) (*appv1.UpdateAdminPaymentResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Payment not found")
}
newStatus := strings.ToUpper(strings.TrimSpace(req.GetStatus()))
if newStatus == "" {
newStatus = "SUCCESS"
}
if newStatus != "SUCCESS" && newStatus != "FAILED" && newStatus != "PENDING" {
return nil, status.Error(codes.InvalidArgument, "Invalid payment status")
}
var payment model.Payment
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&payment).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Payment not found")
}
return nil, status.Error(codes.Internal, "Failed to update payment")
}
currentStatus := strings.ToUpper(normalizePaymentStatus(payment.Status))
if currentStatus != newStatus {
if (currentStatus == "FAILED" || currentStatus == "PENDING") && newStatus == "SUCCESS" {
return nil, status.Error(codes.InvalidArgument, "Cannot transition payment to SUCCESS from admin update; recreate through the payment flow instead")
}
payment.Status = model.StringPtr(newStatus)
if err := s.db.WithContext(ctx).Save(&payment).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to update payment")
}
}
payload, err := s.buildAdminPayment(ctx, &payment)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update payment")
}
return &appv1.UpdateAdminPaymentResponse{Payment: payload}, nil
}
func (s *appServices) ListAdminPlans(ctx context.Context, _ *appv1.ListAdminPlansRequest) (*appv1.ListAdminPlansResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
var plans []model.Plan
if err := s.db.WithContext(ctx).Order("price ASC").Find(&plans).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list plans")
}
items := make([]*appv1.AdminPlan, 0, len(plans))
for i := range plans {
payload, err := s.buildAdminPlan(ctx, &plans[i])
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list plans")
}
items = append(items, payload)
}
return &appv1.ListAdminPlansResponse{Plans: items}, nil
}
func (s *appServices) CreateAdminPlan(ctx context.Context, req *appv1.CreateAdminPlanRequest) (*appv1.CreateAdminPlanResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if msg := validateAdminPlanInput(req.GetName(), req.GetCycle(), req.GetPrice(), req.GetStorageLimit(), req.GetUploadLimit()); msg != "" {
return nil, status.Error(codes.InvalidArgument, msg)
}
plan := &model.Plan{
ID: uuid.New().String(),
Name: strings.TrimSpace(req.GetName()),
Description: nullableTrimmedStringPtr(req.Description),
Features: append([]string(nil), req.GetFeatures()...),
Price: req.GetPrice(),
Cycle: strings.TrimSpace(req.GetCycle()),
StorageLimit: req.GetStorageLimit(),
UploadLimit: req.GetUploadLimit(),
DurationLimit: 0,
QualityLimit: "",
IsActive: model.BoolPtr(req.GetIsActive()),
}
if err := s.db.WithContext(ctx).Create(plan).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to create plan")
}
payload, err := s.buildAdminPlan(ctx, plan)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create plan")
}
return &appv1.CreateAdminPlanResponse{Plan: payload}, nil
}
func (s *appServices) UpdateAdminPlan(ctx context.Context, req *appv1.UpdateAdminPlanRequest) (*appv1.UpdateAdminPlanResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Plan not found")
}
if msg := validateAdminPlanInput(req.GetName(), req.GetCycle(), req.GetPrice(), req.GetStorageLimit(), req.GetUploadLimit()); msg != "" {
return nil, status.Error(codes.InvalidArgument, msg)
}
var plan model.Plan
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Plan not found")
}
return nil, status.Error(codes.Internal, "Failed to update plan")
}
plan.Name = strings.TrimSpace(req.GetName())
plan.Description = nullableTrimmedStringPtr(req.Description)
plan.Features = append([]string(nil), req.GetFeatures()...)
plan.Price = req.GetPrice()
plan.Cycle = strings.TrimSpace(req.GetCycle())
plan.StorageLimit = req.GetStorageLimit()
plan.UploadLimit = req.GetUploadLimit()
plan.IsActive = model.BoolPtr(req.GetIsActive())
if err := s.db.WithContext(ctx).Save(&plan).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to update plan")
}
payload, err := s.buildAdminPlan(ctx, &plan)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update plan")
}
return &appv1.UpdateAdminPlanResponse{Plan: payload}, nil
}
func (s *appServices) DeleteAdminPlan(ctx context.Context, req *appv1.DeleteAdminPlanRequest) (*appv1.DeleteAdminPlanResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Plan not found")
}
var plan model.Plan
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Plan not found")
}
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
var paymentCount int64
if err := s.db.WithContext(ctx).Model(&model.Payment{}).Where("plan_id = ?", id).Count(&paymentCount).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
var subscriptionCount int64
if err := s.db.WithContext(ctx).Model(&model.PlanSubscription{}).Where("plan_id = ?", id).Count(&subscriptionCount).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
if paymentCount > 0 || subscriptionCount > 0 {
inactive := false
if err := s.db.WithContext(ctx).Model(&model.Plan{}).Where("id = ?", id).Update("is_active", inactive).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to deactivate plan")
}
return &appv1.DeleteAdminPlanResponse{Message: "Plan deactivated", Mode: "deactivated"}, nil
}
if err := s.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Plan{}).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to delete plan")
}
return &appv1.DeleteAdminPlanResponse{Message: "Plan deleted", Mode: "deleted"}, nil
}
func (s *appServices) ListAdminAdTemplates(ctx context.Context, req *appv1.ListAdminAdTemplatesRequest) (*appv1.ListAdminAdTemplatesResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
limitInt := int(limit)
search := strings.TrimSpace(protoStringValue(req.Search))
userID := strings.TrimSpace(protoStringValue(req.UserId))
db := s.db.WithContext(ctx).Model(&model.AdTemplate{})
if search != "" {
like := "%" + search + "%"
db = db.Where("name ILIKE ?", like)
}
if userID != "" {
db = db.Where("user_id = ?", userID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list ad templates")
}
var templates []model.AdTemplate
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&templates).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list ad templates")
}
items := make([]*appv1.AdminAdTemplate, 0, len(templates))
for i := range templates {
payload, err := s.buildAdminAdTemplate(ctx, &templates[i])
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list ad templates")
}
items = append(items, payload)
}
return &appv1.ListAdminAdTemplatesResponse{
Templates: items,
Total: total,
Page: page,
Limit: limit,
}, nil
}
func (s *appServices) GetAdminAdTemplate(ctx context.Context, req *appv1.GetAdminAdTemplateRequest) (*appv1.GetAdminAdTemplateResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
var item model.AdTemplate
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
return nil, status.Error(codes.Internal, "Failed to load ad template")
}
payload, err := s.buildAdminAdTemplate(ctx, &item)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to load ad template")
}
return &appv1.GetAdminAdTemplateResponse{Template: payload}, nil
}
func (s *appServices) CreateAdminAdTemplate(ctx context.Context, req *appv1.CreateAdminAdTemplateRequest) (*appv1.CreateAdminAdTemplateResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
duration := req.Duration
if msg := validateAdminAdTemplateInput(req.GetUserId(), req.GetName(), req.GetVastTagUrl(), req.GetAdFormat(), duration); msg != "" {
return nil, status.Error(codes.InvalidArgument, msg)
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
item := &model.AdTemplate{
ID: uuid.New().String(),
UserID: user.ID,
Name: strings.TrimSpace(req.GetName()),
Description: nullableTrimmedStringPtr(req.Description),
VastTagURL: strings.TrimSpace(req.GetVastTagUrl()),
AdFormat: model.StringPtr(normalizeAdminAdFormatValue(req.GetAdFormat())),
Duration: duration,
IsActive: model.BoolPtr(req.GetIsActive()),
IsDefault: req.GetIsDefault(),
}
if !boolValue(item.IsActive) {
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := s.unsetAdminDefaultTemplates(ctx, tx, item.UserID, ""); err != nil {
return err
}
}
return tx.Create(item).Error
}); err != nil {
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
payload, err := s.buildAdminAdTemplate(ctx, item)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
return &appv1.CreateAdminAdTemplateResponse{Template: payload}, nil
}
func (s *appServices) UpdateAdminAdTemplate(ctx context.Context, req *appv1.UpdateAdminAdTemplateRequest) (*appv1.UpdateAdminAdTemplateResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
duration := req.Duration
if msg := validateAdminAdTemplateInput(req.GetUserId(), req.GetName(), req.GetVastTagUrl(), req.GetAdFormat(), duration); msg != "" {
return nil, status.Error(codes.InvalidArgument, msg)
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.GetUserId())).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
var item model.AdTemplate
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
item.UserID = user.ID
item.Name = strings.TrimSpace(req.GetName())
item.Description = nullableTrimmedStringPtr(req.Description)
item.VastTagURL = strings.TrimSpace(req.GetVastTagUrl())
item.AdFormat = model.StringPtr(normalizeAdminAdFormatValue(req.GetAdFormat()))
item.Duration = duration
item.IsActive = model.BoolPtr(req.GetIsActive())
item.IsDefault = req.GetIsDefault()
if !boolValue(item.IsActive) {
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := s.unsetAdminDefaultTemplates(ctx, tx, item.UserID, item.ID); err != nil {
return err
}
}
return tx.Save(&item).Error
}); err != nil {
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
payload, err := s.buildAdminAdTemplate(ctx, &item)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
return &appv1.UpdateAdminAdTemplateResponse{Template: payload}, nil
}
func (s *appServices) DeleteAdminAdTemplate(ctx context.Context, req *appv1.DeleteAdminAdTemplateRequest) (*appv1.MessageResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("ad_template_id = ?", id).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
res := tx.Where("id = ?", id).Delete(&model.AdTemplate{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
return nil, status.Error(codes.Internal, "Failed to delete ad template")
}
return &appv1.MessageResponse{Message: "Ad template deleted"}, nil
}

View File

@@ -0,0 +1,201 @@
package app
import (
"context"
"encoding/json"
"errors"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
appv1 "stream.api/internal/gen/proto/app/v1"
"stream.api/internal/video/runtime/services"
)
func (s *appServices) ListAdminJobs(ctx context.Context, req *appv1.ListAdminJobsRequest) (*appv1.ListAdminJobsResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.jobService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
offset := int(req.GetOffset())
limit := int(req.GetLimit())
if offset < 0 {
offset = 0
}
if limit <= 0 || limit > 100 {
limit = 20
}
var (
result *services.PaginatedJobs
err error
)
if agentID := strings.TrimSpace(req.GetAgentId()); agentID != "" {
result, err = s.jobService.ListJobsByAgent(ctx, agentID, offset, limit)
} else {
result, err = s.jobService.ListJobs(ctx, offset, limit)
}
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list jobs")
}
jobs := make([]*appv1.AdminJob, 0, len(result.Jobs))
for _, job := range result.Jobs {
jobs = append(jobs, buildAdminJob(job))
}
return &appv1.ListAdminJobsResponse{
Jobs: jobs,
Total: result.Total,
Offset: int32(result.Offset),
Limit: int32(result.Limit),
HasMore: result.HasMore,
}, nil
}
func (s *appServices) GetAdminJob(ctx context.Context, req *appv1.GetAdminJobRequest) (*appv1.GetAdminJobResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.jobService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Job not found")
}
job, err := s.jobService.GetJob(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Job not found")
}
return nil, status.Error(codes.Internal, "Failed to load job")
}
return &appv1.GetAdminJobResponse{Job: buildAdminJob(job)}, nil
}
func (s *appServices) GetAdminJobLogs(ctx context.Context, req *appv1.GetAdminJobLogsRequest) (*appv1.GetAdminJobLogsResponse, error) {
response, err := s.GetAdminJob(ctx, &appv1.GetAdminJobRequest{Id: req.GetId()})
if err != nil {
return nil, err
}
return &appv1.GetAdminJobLogsResponse{Logs: response.GetJob().GetLogs()}, nil
}
func (s *appServices) CreateAdminJob(ctx context.Context, req *appv1.CreateAdminJobRequest) (*appv1.CreateAdminJobResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.jobService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
command := strings.TrimSpace(req.GetCommand())
if command == "" {
return nil, status.Error(codes.InvalidArgument, "Command is required")
}
image := strings.TrimSpace(req.GetImage())
if image == "" {
image = "alpine"
}
name := strings.TrimSpace(req.GetName())
if name == "" {
name = command
}
payload, err := json.Marshal(map[string]any{
"image": image,
"commands": []string{command},
"environment": req.GetEnv(),
})
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create job payload")
}
job, err := s.jobService.CreateJob(ctx, strings.TrimSpace(req.GetUserId()), name, payload, int(req.GetPriority()), req.GetTimeLimit())
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create job")
}
return &appv1.CreateAdminJobResponse{Job: buildAdminJob(job)}, nil
}
func (s *appServices) CancelAdminJob(ctx context.Context, req *appv1.CancelAdminJobRequest) (*appv1.CancelAdminJobResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.jobService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Job not found")
}
if err := s.jobService.CancelJob(ctx, id); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, status.Error(codes.NotFound, "Job not found")
}
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
return &appv1.CancelAdminJobResponse{Status: "cancelled", JobId: id}, nil
}
func (s *appServices) RetryAdminJob(ctx context.Context, req *appv1.RetryAdminJobRequest) (*appv1.RetryAdminJobResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.jobService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Job not found")
}
job, err := s.jobService.RetryJob(ctx, id)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, status.Error(codes.NotFound, "Job not found")
}
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
return &appv1.RetryAdminJobResponse{Job: buildAdminJob(job)}, nil
}
func (s *appServices) ListAdminAgents(ctx context.Context, _ *appv1.ListAdminAgentsRequest) (*appv1.ListAdminAgentsResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.agentRuntime == nil {
return nil, status.Error(codes.Unavailable, "Agent runtime is unavailable")
}
items := s.agentRuntime.ListAgentsWithStats()
agents := make([]*appv1.AdminAgent, 0, len(items))
for _, item := range items {
agents = append(agents, buildAdminAgent(item))
}
return &appv1.ListAdminAgentsResponse{Agents: agents}, nil
}
func (s *appServices) RestartAdminAgent(ctx context.Context, req *appv1.RestartAdminAgentRequest) (*appv1.AdminAgentCommandResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.agentRuntime == nil {
return nil, status.Error(codes.Unavailable, "Agent runtime is unavailable")
}
if !s.agentRuntime.SendCommand(strings.TrimSpace(req.GetId()), "restart") {
return nil, status.Error(codes.Unavailable, "Agent not active or command channel full")
}
return &appv1.AdminAgentCommandResponse{Status: "restart command sent"}, nil
}
func (s *appServices) UpdateAdminAgent(ctx context.Context, req *appv1.UpdateAdminAgentRequest) (*appv1.AdminAgentCommandResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.agentRuntime == nil {
return nil, status.Error(codes.Unavailable, "Agent runtime is unavailable")
}
if !s.agentRuntime.SendCommand(strings.TrimSpace(req.GetId()), "update") {
return nil, status.Error(codes.Unavailable, "Agent not active or command channel full")
}
return &appv1.AdminAgentCommandResponse{Status: "update command sent"}, nil
}

View File

@@ -0,0 +1,579 @@
package app
import (
"context"
"errors"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"stream.api/internal/database/model"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func (s *appServices) GetAdminDashboard(ctx context.Context, _ *appv1.GetAdminDashboardRequest) (*appv1.GetAdminDashboardResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
dashboard := &appv1.AdminDashboard{}
db := s.db.WithContext(ctx)
db.Model(&model.User{}).Count(&dashboard.TotalUsers)
db.Model(&model.Video{}).Count(&dashboard.TotalVideos)
db.Model(&model.User{}).Select("COALESCE(SUM(storage_used), 0)").Row().Scan(&dashboard.TotalStorageUsed)
db.Model(&model.Payment{}).Count(&dashboard.TotalPayments)
db.Model(&model.Payment{}).Where("status = ?", "SUCCESS").Select("COALESCE(SUM(amount), 0)").Row().Scan(&dashboard.TotalRevenue)
db.Model(&model.PlanSubscription{}).Where("expires_at > ?", time.Now()).Count(&dashboard.ActiveSubscriptions)
db.Model(&model.AdTemplate{}).Count(&dashboard.TotalAdTemplates)
today := time.Now().Truncate(24 * time.Hour)
db.Model(&model.User{}).Where("created_at >= ?", today).Count(&dashboard.NewUsersToday)
db.Model(&model.Video{}).Where("created_at >= ?", today).Count(&dashboard.NewVideosToday)
return &appv1.GetAdminDashboardResponse{Dashboard: dashboard}, nil
}
func (s *appServices) ListAdminUsers(ctx context.Context, req *appv1.ListAdminUsersRequest) (*appv1.ListAdminUsersResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
limitInt := int(limit)
search := strings.TrimSpace(req.GetSearch())
role := strings.TrimSpace(req.GetRole())
db := s.db.WithContext(ctx).Model(&model.User{})
if search != "" {
like := "%" + search + "%"
db = db.Where("email ILIKE ? OR username ILIKE ?", like, like)
}
if role != "" {
db = db.Where("UPPER(role) = ?", strings.ToUpper(role))
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list users")
}
var users []model.User
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&users).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list users")
}
items := make([]*appv1.AdminUser, 0, len(users))
for _, user := range users {
payload, err := s.buildAdminUser(ctx, &user)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list users")
}
items = append(items, payload)
}
return &appv1.ListAdminUsersResponse{Users: items, Total: total, Page: page, Limit: limit}, nil
}
func (s *appServices) GetAdminUser(ctx context.Context, req *appv1.GetAdminUserRequest) (*appv1.GetAdminUserResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "User not found")
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to get user")
}
payload, err := s.buildAdminUser(ctx, &user)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to get user")
}
var subscription model.PlanSubscription
var subscriptionPayload *appv1.PlanSubscription
if err := s.db.WithContext(ctx).Where("user_id = ?", id).Order("created_at DESC").First(&subscription).Error; err == nil {
subscriptionPayload = toProtoPlanSubscription(&subscription)
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.Internal, "Failed to get user")
}
return &appv1.GetAdminUserResponse{User: &appv1.AdminUserDetail{User: payload, Subscription: subscriptionPayload}}, nil
}
func (s *appServices) CreateAdminUser(ctx context.Context, req *appv1.CreateAdminUserRequest) (*appv1.CreateAdminUserResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
email := strings.TrimSpace(req.GetEmail())
password := req.GetPassword()
if email == "" || password == "" {
return nil, status.Error(codes.InvalidArgument, "Email and password are required")
}
role := normalizeAdminRoleValue(req.GetRole())
if !isValidAdminRoleValue(role) {
return nil, status.Error(codes.InvalidArgument, "Invalid role. Must be USER, ADMIN, or BLOCK")
}
planID := nullableTrimmedString(req.PlanId)
if err := s.ensurePlanExists(ctx, planID); err != nil {
return nil, err
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to hash password")
}
user := &model.User{
ID: uuid.New().String(),
Email: email,
Password: model.StringPtr(string(hashedPassword)),
Username: nullableTrimmedString(req.Username),
Role: model.StringPtr(role),
PlanID: planID,
}
if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, status.Error(codes.AlreadyExists, "Email already registered")
}
return nil, status.Error(codes.Internal, "Failed to create user")
}
payload, err := s.buildAdminUser(ctx, user)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create user")
}
return &appv1.CreateAdminUserResponse{User: payload}, nil
}
func (s *appServices) UpdateAdminUser(ctx context.Context, req *appv1.UpdateAdminUserRequest) (*appv1.UpdateAdminUserResponse, error) {
adminResult, err := s.requireAdmin(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "User not found")
}
updates := map[string]interface{}{}
if req.Email != nil {
email := strings.TrimSpace(req.GetEmail())
if email == "" {
return nil, status.Error(codes.InvalidArgument, "Email is required")
}
updates["email"] = email
}
if req.Username != nil {
updates["username"] = nullableTrimmedString(req.Username)
}
if req.Role != nil {
role := normalizeAdminRoleValue(req.GetRole())
if !isValidAdminRoleValue(role) {
return nil, status.Error(codes.InvalidArgument, "Invalid role. Must be USER, ADMIN, or BLOCK")
}
if id == adminResult.UserID && role != "ADMIN" {
return nil, status.Error(codes.InvalidArgument, "Cannot change your own role")
}
updates["role"] = role
}
if req.PlanId != nil {
planID := nullableTrimmedString(req.PlanId)
if err := s.ensurePlanExists(ctx, planID); err != nil {
return nil, err
}
updates["plan_id"] = planID
}
if req.Password != nil {
if strings.TrimSpace(req.GetPassword()) == "" {
return nil, status.Error(codes.InvalidArgument, "Password must not be empty")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.GetPassword()), bcrypt.DefaultCost)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to hash password")
}
updates["password"] = string(hashedPassword)
}
if len(updates) == 0 {
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to update user")
}
payload, err := s.buildAdminUser(ctx, &user)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update user")
}
return &appv1.UpdateAdminUserResponse{User: payload}, nil
}
result := s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return nil, status.Error(codes.AlreadyExists, "Email already registered")
}
return nil, status.Error(codes.Internal, "Failed to update user")
}
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "User not found")
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to update user")
}
payload, err := s.buildAdminUser(ctx, &user)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update user")
}
return &appv1.UpdateAdminUserResponse{User: payload}, nil
}
func (s *appServices) UpdateAdminUserRole(ctx context.Context, req *appv1.UpdateAdminUserRoleRequest) (*appv1.UpdateAdminUserRoleResponse, error) {
adminResult, err := s.requireAdmin(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "User not found")
}
if id == adminResult.UserID {
return nil, status.Error(codes.InvalidArgument, "Cannot change your own role")
}
role := normalizeAdminRoleValue(req.GetRole())
if !isValidAdminRoleValue(role) {
return nil, status.Error(codes.InvalidArgument, "Invalid role. Must be USER, ADMIN, or BLOCK")
}
result := s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Update("role", role)
if result.Error != nil {
return nil, status.Error(codes.Internal, "Failed to update role")
}
if result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "User not found")
}
return &appv1.UpdateAdminUserRoleResponse{Message: "Role updated", Role: role}, nil
}
func (s *appServices) DeleteAdminUser(ctx context.Context, req *appv1.DeleteAdminUserRequest) (*appv1.MessageResponse, error) {
adminResult, err := s.requireAdmin(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "User not found")
}
if id == adminResult.UserID {
return nil, status.Error(codes.InvalidArgument, "Cannot delete your own account")
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to find user")
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
tables := []struct {
model interface{}
where string
}{
{&model.VideoAdConfig{}, "user_id = ?"},
{&model.AdTemplate{}, "user_id = ?"},
{&model.Notification{}, "user_id = ?"},
{&model.Domain{}, "user_id = ?"},
{&model.WalletTransaction{}, "user_id = ?"},
{&model.PlanSubscription{}, "user_id = ?"},
{&model.UserPreference{}, "user_id = ?"},
{&model.Video{}, "user_id = ?"},
{&model.Payment{}, "user_id = ?"},
}
for _, item := range tables {
if err := tx.Where(item.where, id).Delete(item.model).Error; err != nil {
return err
}
}
return tx.Where("id = ?", id).Delete(&model.User{}).Error
})
if err != nil {
return nil, status.Error(codes.Internal, "Failed to delete user")
}
return messageResponse("User deleted"), nil
}
func (s *appServices) ListAdminVideos(ctx context.Context, req *appv1.ListAdminVideosRequest) (*appv1.ListAdminVideosResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
limitInt := int(limit)
search := strings.TrimSpace(req.GetSearch())
userID := strings.TrimSpace(req.GetUserId())
statusFilter := strings.TrimSpace(req.GetStatus())
db := s.db.WithContext(ctx).Model(&model.Video{})
if search != "" {
like := "%" + search + "%"
db = db.Where("title ILIKE ?", like)
}
if userID != "" {
db = db.Where("user_id = ?", userID)
}
if statusFilter != "" && !strings.EqualFold(statusFilter, "all") {
db = db.Where("status = ?", normalizeVideoStatusValue(statusFilter))
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list videos")
}
var videos []model.Video
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&videos).Error; err != nil {
return nil, status.Error(codes.Internal, "Failed to list videos")
}
items := make([]*appv1.AdminVideo, 0, len(videos))
for _, video := range videos {
payload, err := s.buildAdminVideo(ctx, &video)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list videos")
}
items = append(items, payload)
}
return &appv1.ListAdminVideosResponse{Videos: items, Total: total, Page: page, Limit: limit}, nil
}
func (s *appServices) GetAdminVideo(ctx context.Context, req *appv1.GetAdminVideoRequest) (*appv1.GetAdminVideoResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Video not found")
}
var video model.Video
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Video not found")
}
return nil, status.Error(codes.Internal, "Failed to get video")
}
payload, err := s.buildAdminVideo(ctx, &video)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to get video")
}
return &appv1.GetAdminVideoResponse{Video: payload}, nil
}
func (s *appServices) CreateAdminVideo(ctx context.Context, req *appv1.CreateAdminVideoRequest) (*appv1.CreateAdminVideoResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
userID := strings.TrimSpace(req.GetUserId())
title := strings.TrimSpace(req.GetTitle())
videoURL := strings.TrimSpace(req.GetUrl())
if userID == "" || title == "" || videoURL == "" {
return nil, status.Error(codes.InvalidArgument, "User ID, title, and URL are required")
}
if req.GetSize() < 0 {
return nil, status.Error(codes.InvalidArgument, "Size must be greater than or equal to 0")
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to create video")
}
statusValue := normalizeVideoStatusValue(req.GetStatus())
processingStatus := strings.ToUpper(statusValue)
storageType := detectStorageType(videoURL)
video := &model.Video{
ID: uuid.New().String(),
UserID: user.ID,
Name: title,
Title: title,
Description: nullableTrimmedString(req.Description),
URL: videoURL,
Size: req.GetSize(),
Duration: req.GetDuration(),
Format: strings.TrimSpace(req.GetFormat()),
Status: model.StringPtr(statusValue),
ProcessingStatus: model.StringPtr(processingStatus),
StorageType: model.StringPtr(storageType),
}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(video).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
return err
}
return s.saveAdminVideoAdConfig(ctx, tx, video.ID, user.ID, nullableTrimmedString(req.AdTemplateId))
})
if err != nil {
if strings.Contains(err.Error(), "Ad template not found") {
return nil, status.Error(codes.InvalidArgument, "Ad template not found")
}
return nil, status.Error(codes.Internal, "Failed to create video")
}
payload, err := s.buildAdminVideo(ctx, video)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create video")
}
return &appv1.CreateAdminVideoResponse{Video: payload}, nil
}
func (s *appServices) UpdateAdminVideo(ctx context.Context, req *appv1.UpdateAdminVideoRequest) (*appv1.UpdateAdminVideoResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
userID := strings.TrimSpace(req.GetUserId())
title := strings.TrimSpace(req.GetTitle())
videoURL := strings.TrimSpace(req.GetUrl())
if id == "" {
return nil, status.Error(codes.NotFound, "Video not found")
}
if userID == "" || title == "" || videoURL == "" {
return nil, status.Error(codes.InvalidArgument, "User ID, title, and URL are required")
}
if req.GetSize() < 0 {
return nil, status.Error(codes.InvalidArgument, "Size must be greater than or equal to 0")
}
var video model.Video
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Video not found")
}
return nil, status.Error(codes.Internal, "Failed to update video")
}
var user model.User
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.InvalidArgument, "User not found")
}
return nil, status.Error(codes.Internal, "Failed to update video")
}
oldSize := video.Size
oldUserID := video.UserID
statusValue := normalizeVideoStatusValue(req.GetStatus())
processingStatus := strings.ToUpper(statusValue)
video.UserID = user.ID
video.Name = title
video.Title = title
video.Description = nullableTrimmedString(req.Description)
video.URL = videoURL
video.Size = req.GetSize()
video.Duration = req.GetDuration()
video.Format = strings.TrimSpace(req.GetFormat())
video.Status = model.StringPtr(statusValue)
video.ProcessingStatus = model.StringPtr(processingStatus)
video.StorageType = model.StringPtr(detectStorageType(videoURL))
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&video).Error; err != nil {
return err
}
if oldUserID == user.ID {
delta := video.Size - oldSize
if delta != 0 {
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used + ?, 0)", delta)).Error; err != nil {
return err
}
}
} else {
if err := tx.Model(&model.User{}).Where("id = ?", oldUserID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", oldSize)).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
return err
}
}
if oldUserID != user.ID {
if err := tx.Model(&model.VideoAdConfig{}).Where("video_id = ?", video.ID).Update("user_id", user.ID).Error; err != nil {
return err
}
}
return s.saveAdminVideoAdConfig(ctx, tx, video.ID, user.ID, nullableTrimmedString(req.AdTemplateId))
})
if err != nil {
if strings.Contains(err.Error(), "Ad template not found") {
return nil, status.Error(codes.InvalidArgument, "Ad template not found")
}
return nil, status.Error(codes.Internal, "Failed to update video")
}
payload, err := s.buildAdminVideo(ctx, &video)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to update video")
}
return &appv1.UpdateAdminVideoResponse{Video: payload}, nil
}
func (s *appServices) DeleteAdminVideo(ctx context.Context, req *appv1.DeleteAdminVideoRequest) (*appv1.MessageResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Video not found")
}
var video model.Video
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Video not found")
}
return nil, status.Error(codes.Internal, "Failed to find video")
}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("video_id = ?", video.ID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("id = ?", video.ID).Delete(&model.Video{}).Error; err != nil {
return err
}
return tx.Model(&model.User{}).Where("id = ?", video.UserID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", video.Size)).Error
})
if err != nil {
return nil, status.Error(codes.Internal, "Failed to delete video")
}
return messageResponse("Video deleted"), nil
}

View File

@@ -0,0 +1,300 @@
package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
authapi "stream.api/internal/api/auth"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func (s *appServices) Login(ctx context.Context, req *appv1.LoginRequest) (*appv1.LoginResponse, error) {
email := strings.TrimSpace(req.GetEmail())
password := req.GetPassword()
if email == "" || password == "" {
return nil, status.Error(codes.InvalidArgument, "Email and password are required")
}
u := query.User
user, err := u.WithContext(ctx).Where(u.Email.Eq(email)).First()
if err != nil {
return nil, status.Error(codes.Unauthenticated, "Invalid credentials")
}
if user.Password == nil || strings.TrimSpace(*user.Password) == "" {
return nil, status.Error(codes.Unauthenticated, "Please login with Google")
}
if err := bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)); err != nil {
return nil, status.Error(codes.Unauthenticated, "Invalid credentials")
}
if err := s.issueSessionCookies(ctx, user); err != nil {
return nil, err
}
payload, err := authapi.BuildUserPayload(ctx, s.db, user)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to build user payload")
}
return &appv1.LoginResponse{User: toProtoUser(payload)}, nil
}
func (s *appServices) Register(ctx context.Context, req *appv1.RegisterRequest) (*appv1.RegisterResponse, error) {
email := strings.TrimSpace(req.GetEmail())
username := strings.TrimSpace(req.GetUsername())
password := req.GetPassword()
if email == "" || username == "" || password == "" {
return nil, status.Error(codes.InvalidArgument, "Username, email and password are required")
}
u := query.User
count, err := u.WithContext(ctx).Where(u.Email.Eq(email)).Count()
if err != nil {
s.logger.Error("Failed to check existing user", "error", err)
return nil, status.Error(codes.Internal, "Failed to register")
}
if count > 0 {
return nil, status.Error(codes.InvalidArgument, "Email already registered")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to register")
}
role := "USER"
passwordHash := string(hashedPassword)
newUser := &model.User{
ID: uuid.New().String(),
Email: email,
Password: &passwordHash,
Username: &username,
Role: &role,
}
if err := u.WithContext(ctx).Create(newUser); err != nil {
s.logger.Error("Failed to create user", "error", err)
return nil, status.Error(codes.Internal, "Failed to register")
}
payload, err := authapi.BuildUserPayload(ctx, s.db, newUser)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to build user payload")
}
return &appv1.RegisterResponse{User: toProtoUser(payload)}, nil
}
func (s *appServices) Logout(ctx context.Context, _ *appv1.LogoutRequest) (*appv1.MessageResponse, error) {
return messageResponse("Logged out"), nil
}
func (s *appServices) ChangePassword(ctx context.Context, req *appv1.ChangePasswordRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
currentPassword := req.GetCurrentPassword()
newPassword := req.GetNewPassword()
if currentPassword == "" || newPassword == "" {
return nil, status.Error(codes.InvalidArgument, "Current password and new password are required")
}
if currentPassword == newPassword {
return nil, status.Error(codes.InvalidArgument, "New password must be different")
}
if result.User.Password == nil || strings.TrimSpace(*result.User.Password) == "" {
return nil, status.Error(codes.InvalidArgument, "This account does not have a local password")
}
if err := bcrypt.CompareHashAndPassword([]byte(*result.User.Password), []byte(currentPassword)); err != nil {
return nil, status.Error(codes.InvalidArgument, "Current password is incorrect")
}
newHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to change password")
}
if _, err := query.User.WithContext(ctx).
Where(query.User.ID.Eq(result.UserID)).
Update(query.User.Password, string(newHash)); err != nil {
s.logger.Error("Failed to change password", "error", err)
return nil, status.Error(codes.Internal, "Failed to change password")
}
return messageResponse("Password changed successfully"), nil
}
func (s *appServices) ForgotPassword(ctx context.Context, req *appv1.ForgotPasswordRequest) (*appv1.MessageResponse, error) {
email := strings.TrimSpace(req.GetEmail())
if email == "" {
return nil, status.Error(codes.InvalidArgument, "Email is required")
}
u := query.User
user, err := u.WithContext(ctx).Where(u.Email.Eq(email)).First()
if err != nil {
return messageResponse("If email exists, a reset link has been sent"), nil
}
tokenID := uuid.New().String()
if err := s.cache.Set(ctx, "reset_pw:"+tokenID, user.ID, 15*time.Minute); err != nil {
s.logger.Error("Failed to set reset token", "error", err)
return nil, status.Error(codes.Internal, "Try again later")
}
s.logger.Info("Generated password reset token", "email", email, "token", tokenID)
return messageResponse("If email exists, a reset link has been sent"), nil
}
func (s *appServices) ResetPassword(ctx context.Context, req *appv1.ResetPasswordRequest) (*appv1.MessageResponse, error) {
resetToken := strings.TrimSpace(req.GetToken())
newPassword := req.GetNewPassword()
if resetToken == "" || newPassword == "" {
return nil, status.Error(codes.InvalidArgument, "Token and new password are required")
}
userID, err := s.cache.Get(ctx, "reset_pw:"+resetToken)
if err != nil || strings.TrimSpace(userID) == "" {
return nil, status.Error(codes.InvalidArgument, "Invalid or expired token")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return nil, status.Error(codes.Internal, "Internal error")
}
if _, err := query.User.WithContext(ctx).
Where(query.User.ID.Eq(userID)).
Update(query.User.Password, string(hashedPassword)); err != nil {
s.logger.Error("Failed to update password", "error", err)
return nil, status.Error(codes.Internal, "Failed to update password")
}
_ = s.cache.Del(ctx, "reset_pw:"+resetToken)
return messageResponse("Password reset successfully"), nil
}
func (s *appServices) GetGoogleLoginUrl(ctx context.Context, _ *appv1.GetGoogleLoginUrlRequest) (*appv1.GetGoogleLoginUrlResponse, error) {
if err := s.authenticator.RequireInternalCall(ctx); err != nil {
return nil, err
}
if s.googleOauth == nil || strings.TrimSpace(s.googleOauth.ClientID) == "" || strings.TrimSpace(s.googleOauth.RedirectURL) == "" {
return nil, status.Error(codes.FailedPrecondition, "Google OAuth is not configured")
}
state, err := generateOAuthState()
if err != nil {
s.logger.Error("Failed to generate Google OAuth state", "error", err)
return nil, status.Error(codes.Internal, "Failed to start Google login")
}
if err := s.cache.Set(ctx, googleOAuthStateCacheKey(state), "1", s.googleStateTTL); err != nil {
s.logger.Error("Failed to persist Google OAuth state", "error", err)
return nil, status.Error(codes.Internal, "Failed to start Google login")
}
loginURL := s.googleOauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
return &appv1.GetGoogleLoginUrlResponse{Url: loginURL}, nil
}
func (s *appServices) CompleteGoogleLogin(ctx context.Context, req *appv1.CompleteGoogleLoginRequest) (*appv1.CompleteGoogleLoginResponse, error) {
if err := s.authenticator.RequireInternalCall(ctx); err != nil {
return nil, err
}
if s.googleOauth == nil || strings.TrimSpace(s.googleOauth.ClientID) == "" || strings.TrimSpace(s.googleOauth.RedirectURL) == "" {
return nil, status.Error(codes.FailedPrecondition, "Google OAuth is not configured")
}
code := strings.TrimSpace(req.GetCode())
if code == "" {
return nil, status.Error(codes.InvalidArgument, "Code is required")
}
tokenResp, err := s.googleOauth.Exchange(ctx, code)
if err != nil {
s.logger.Error("Failed to exchange Google OAuth token", "error", err)
return nil, status.Error(codes.Unauthenticated, "exchange_failed")
}
client := s.googleOauth.Client(ctx, tokenResp)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
s.logger.Error("Failed to fetch Google user info", "error", err)
return nil, status.Error(codes.Unauthenticated, "userinfo_failed")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
s.logger.Error("Google user info returned non-200", "status", resp.StatusCode)
return nil, status.Error(codes.Unauthenticated, "userinfo_failed")
}
var googleUser struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
if err := json.NewDecoder(resp.Body).Decode(&googleUser); err != nil {
s.logger.Error("Failed to decode Google user info", "error", err)
return nil, status.Error(codes.Internal, "userinfo_parse_failed")
}
email := strings.TrimSpace(strings.ToLower(googleUser.Email))
if email == "" {
return nil, status.Error(codes.InvalidArgument, "missing_email")
}
u := query.User
user, err := u.WithContext(ctx).Where(u.Email.Eq(email)).First()
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Error("Failed to load Google user", "error", err)
return nil, status.Error(codes.Internal, "load_user_failed")
}
role := "USER"
user = &model.User{
ID: uuid.New().String(),
Email: email,
Username: stringPointerOrNil(googleUser.Name),
GoogleID: stringPointerOrNil(googleUser.ID),
Avatar: stringPointerOrNil(googleUser.Picture),
Role: &role,
}
if err := u.WithContext(ctx).Create(user); err != nil {
s.logger.Error("Failed to create Google user", "error", err)
return nil, status.Error(codes.Internal, "create_user_failed")
}
} else {
updates := map[string]interface{}{}
if user.GoogleID == nil || strings.TrimSpace(*user.GoogleID) == "" {
updates["google_id"] = googleUser.ID
}
if user.Avatar == nil || strings.TrimSpace(*user.Avatar) == "" {
updates["avatar"] = googleUser.Picture
}
if user.Username == nil || strings.TrimSpace(*user.Username) == "" {
updates["username"] = googleUser.Name
}
if len(updates) > 0 {
if err := s.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", user.ID).Updates(updates).Error; err != nil {
s.logger.Error("Failed to update Google user", "error", err)
return nil, status.Error(codes.Internal, "update_user_failed")
}
user, err = u.WithContext(ctx).Where(u.ID.Eq(user.ID)).First()
if err != nil {
s.logger.Error("Failed to reload Google user", "error", err)
return nil, status.Error(codes.Internal, "reload_user_failed")
}
}
}
if err := s.issueSessionCookies(ctx, user); err != nil {
return nil, status.Error(codes.Internal, "session_failed")
}
payload, err := authapi.BuildUserPayload(ctx, s.db, user)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to build user payload")
}
return &appv1.CompleteGoogleLoginResponse{User: toProtoUser(payload)}, nil
}

View File

@@ -0,0 +1,203 @@
package app
import (
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"gorm.io/gorm"
"stream.api/internal/config"
appv1 "stream.api/internal/gen/proto/app/v1"
"stream.api/internal/middleware"
videogrpc "stream.api/internal/video/runtime/grpc"
"stream.api/internal/video/runtime/services"
"stream.api/pkg/cache"
"stream.api/pkg/logger"
"stream.api/pkg/storage"
"stream.api/pkg/token"
)
const adTemplateUpgradeRequiredMessage = "Upgrade required to manage Ads & VAST"
const (
walletTransactionTypeTopup = "topup"
walletTransactionTypeSubscriptionDebit = "subscription_debit"
paymentMethodWallet = "wallet"
paymentMethodTopup = "topup"
paymentKindSubscription = "subscription"
paymentKindWalletTopup = "wallet_topup"
)
var allowedTermMonths = map[int32]struct{}{
1: {},
3: {},
6: {},
12: {},
}
type Services struct {
AuthServiceServer
AccountServiceServer
PreferencesServiceServer
UsageServiceServer
NotificationsServiceServer
DomainsServiceServer
AdTemplatesServiceServer
PlansServiceServer
PaymentsServiceServer
VideosServiceServer
AdminServiceServer
}
type appServices struct {
appv1.UnimplementedAuthServiceServer
appv1.UnimplementedAccountServiceServer
appv1.UnimplementedPreferencesServiceServer
appv1.UnimplementedUsageServiceServer
appv1.UnimplementedNotificationsServiceServer
appv1.UnimplementedDomainsServiceServer
appv1.UnimplementedAdTemplatesServiceServer
appv1.UnimplementedPlansServiceServer
appv1.UnimplementedPaymentsServiceServer
appv1.UnimplementedVideosServiceServer
appv1.UnimplementedAdminServiceServer
db *gorm.DB
logger logger.Logger
authenticator *middleware.Authenticator
tokenProvider token.Provider
cache cache.Cache
storageProvider storage.Provider
jobService *services.JobService
agentRuntime *videogrpc.Server
googleOauth *oauth2.Config
googleStateTTL time.Duration
}
type paymentRow struct {
ID string `gorm:"column:id"`
Amount float64 `gorm:"column:amount"`
Currency *string `gorm:"column:currency"`
Status *string `gorm:"column:status"`
PlanID *string `gorm:"column:plan_id"`
PlanName *string `gorm:"column:plan_name"`
TermMonths *int32 `gorm:"column:term_months"`
PaymentMethod *string `gorm:"column:payment_method"`
ExpiresAt *time.Time `gorm:"column:expires_at"`
CreatedAt *time.Time `gorm:"column:created_at"`
}
type paymentInvoiceDetails struct {
PlanName string
TermMonths *int32
PaymentMethod string
ExpiresAt *time.Time
WalletAmount float64
TopupAmount float64
}
type apiErrorBody struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func NewServices(c cache.Cache, t token.Provider, db *gorm.DB, l logger.Logger, cfg *config.Config, jobService *services.JobService, agentRuntime *videogrpc.Server) *Services {
var storageProvider storage.Provider
if cfg != nil {
provider, err := storage.NewS3Provider(cfg)
if err != nil {
l.Error("Failed to initialize S3 provider for gRPC app services", "error", err)
} else {
storageProvider = provider
}
}
googleStateTTL := 10 * time.Minute
googleOauth := &oauth2.Config{}
if cfg != nil {
if cfg.Google.StateTTLMinute > 0 {
googleStateTTL = time.Duration(cfg.Google.StateTTLMinute) * time.Minute
}
googleOauth = &oauth2.Config{
ClientID: cfg.Google.ClientID,
ClientSecret: cfg.Google.ClientSecret,
RedirectURL: cfg.Google.RedirectURL,
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
}
service := &appServices{
db: db,
logger: l,
authenticator: middleware.NewAuthenticator(db, l, cfg.Internal.Marker),
tokenProvider: t,
cache: c,
storageProvider: storageProvider,
jobService: jobService,
agentRuntime: agentRuntime,
googleOauth: googleOauth,
googleStateTTL: googleStateTTL,
}
return &Services{
AuthServiceServer: service,
AccountServiceServer: service,
PreferencesServiceServer: service,
UsageServiceServer: service,
NotificationsServiceServer: service,
DomainsServiceServer: service,
AdTemplatesServiceServer: service,
PlansServiceServer: service,
PaymentsServiceServer: service,
VideosServiceServer: service,
AdminServiceServer: service,
}
}
type AuthServiceServer interface {
appv1.AuthServiceServer
}
type AccountServiceServer interface {
appv1.AccountServiceServer
}
type PreferencesServiceServer interface {
appv1.PreferencesServiceServer
}
type UsageServiceServer interface {
appv1.UsageServiceServer
}
type NotificationsServiceServer interface {
appv1.NotificationsServiceServer
}
type DomainsServiceServer interface {
appv1.DomainsServiceServer
}
type AdTemplatesServiceServer interface {
appv1.AdTemplatesServiceServer
}
type PlansServiceServer interface {
appv1.PlansServiceServer
}
type PaymentsServiceServer interface {
appv1.PaymentsServiceServer
}
type VideosServiceServer interface {
appv1.VideosServiceServer
}
type AdminServiceServer interface {
appv1.AdminServiceServer
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,414 @@
package app
import (
"context"
"errors"
"fmt"
"net/http"
"sort"
"strings"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
paymentapi "stream.api/internal/api/payment"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func (s *appServices) CreatePayment(ctx context.Context, req *appv1.CreatePaymentRequest) (*appv1.CreatePaymentResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
planID := strings.TrimSpace(req.GetPlanId())
if planID == "" {
return nil, status.Error(codes.InvalidArgument, "Plan ID is required")
}
if !isAllowedTermMonths(req.GetTermMonths()) {
return nil, status.Error(codes.InvalidArgument, "Term months must be one of 1, 3, 6, or 12")
}
paymentMethod := normalizePaymentMethod(req.GetPaymentMethod())
if paymentMethod == "" {
return nil, status.Error(codes.InvalidArgument, "Payment method must be wallet or topup")
}
var planRecord model.Plan
if err := s.db.WithContext(ctx).Where("id = ?", planID).First(&planRecord).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Plan not found")
}
s.logger.Error("Failed to load plan", "error", err)
return nil, status.Error(codes.Internal, "Failed to create payment")
}
if planRecord.IsActive == nil || !*planRecord.IsActive {
return nil, status.Error(codes.InvalidArgument, "Plan is not active")
}
totalAmount := planRecord.Price * float64(req.GetTermMonths())
if totalAmount < 0 {
return nil, status.Error(codes.InvalidArgument, "Amount must be greater than or equal to 0")
}
statusValue := "SUCCESS"
provider := "INTERNAL"
currency := normalizeCurrency(nil)
transactionID := buildTransactionID("sub")
now := time.Now().UTC()
paymentRecord := &model.Payment{
ID: uuid.New().String(),
UserID: result.UserID,
PlanID: &planRecord.ID,
Amount: totalAmount,
Currency: &currency,
Status: &statusValue,
Provider: &provider,
TransactionID: &transactionID,
}
invoiceID := buildInvoiceID(paymentRecord.ID)
var subscription *model.PlanSubscription
var walletBalance float64
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if _, err := lockUserForUpdate(ctx, tx, result.UserID); err != nil {
return err
}
currentSubscription, err := model.GetLatestPlanSubscription(ctx, tx, result.UserID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
baseExpiry := now
if currentSubscription != nil && currentSubscription.ExpiresAt.After(baseExpiry) {
baseExpiry = currentSubscription.ExpiresAt.UTC()
}
newExpiry := baseExpiry.AddDate(0, int(req.GetTermMonths()), 0)
currentWalletBalance, err := model.GetWalletBalance(ctx, tx, result.UserID)
if err != nil {
return err
}
shortfall := maxFloat(totalAmount-currentWalletBalance, 0)
if paymentMethod == paymentMethodWallet && shortfall > 0 {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Insufficient wallet balance", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
})
}
topupAmount := 0.0
if paymentMethod == paymentMethodTopup {
if req.TopupAmount == nil {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount is required when payment method is topup", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
})
}
topupAmount = maxFloat(req.GetTopupAmount(), 0)
if topupAmount <= 0 {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount must be greater than 0", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
})
}
if topupAmount < shortfall {
return statusErrorWithBody(ctx, codes.InvalidArgument, http.StatusBadRequest, "Top-up amount must be greater than or equal to the required shortfall", map[string]interface{}{
"payment_method": paymentMethod,
"wallet_balance": currentWalletBalance,
"total_amount": totalAmount,
"shortfall": shortfall,
"topup_amount": topupAmount,
})
}
}
if err := tx.Create(paymentRecord).Error; err != nil {
return err
}
walletUsedAmount := totalAmount
if paymentMethod == paymentMethodTopup {
topupTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: result.UserID,
Type: walletTransactionTypeTopup,
Amount: topupAmount,
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Wallet top-up for %s (%d months)", planRecord.Name, req.GetTermMonths())),
PaymentID: &paymentRecord.ID,
PlanID: &planRecord.ID,
TermMonths: int32Ptr(req.GetTermMonths()),
}
if err := tx.Create(topupTransaction).Error; err != nil {
return err
}
}
debitTransaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: result.UserID,
Type: walletTransactionTypeSubscriptionDebit,
Amount: -totalAmount,
Currency: model.StringPtr(currency),
Note: model.StringPtr(fmt.Sprintf("Subscription payment for %s (%d months)", planRecord.Name, req.GetTermMonths())),
PaymentID: &paymentRecord.ID,
PlanID: &planRecord.ID,
TermMonths: int32Ptr(req.GetTermMonths()),
}
if err := tx.Create(debitTransaction).Error; err != nil {
return err
}
subscription = &model.PlanSubscription{
ID: uuid.New().String(),
UserID: result.UserID,
PaymentID: paymentRecord.ID,
PlanID: planRecord.ID,
TermMonths: req.GetTermMonths(),
PaymentMethod: paymentMethod,
WalletAmount: walletUsedAmount,
TopupAmount: topupAmount,
StartedAt: now,
ExpiresAt: newExpiry,
}
if err := tx.Create(subscription).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).
Where("id = ?", result.UserID).
Update("plan_id", planRecord.ID).Error; err != nil {
return err
}
notification := buildSubscriptionNotification(result.UserID, paymentRecord.ID, invoiceID, &planRecord, subscription)
if err := tx.Create(notification).Error; err != nil {
return err
}
walletBalance, err = model.GetWalletBalance(ctx, tx, result.UserID)
if err != nil {
return err
}
return nil
})
if err != nil {
if _, ok := status.FromError(err); ok {
return nil, err
}
s.logger.Error("Failed to create payment", "error", err)
return nil, status.Error(codes.Internal, "Failed to create payment")
}
return &appv1.CreatePaymentResponse{
Payment: toProtoPayment(paymentRecord),
Subscription: toProtoPlanSubscription(subscription),
WalletBalance: walletBalance,
InvoiceId: invoiceID,
Message: "Payment completed successfully",
}, nil
}
func (s *appServices) ListPaymentHistory(ctx context.Context, _ *appv1.ListPaymentHistoryRequest) (*appv1.ListPaymentHistoryResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
var rows []paymentRow
if err := s.db.WithContext(ctx).
Table("payment AS p").
Select("p.id, p.amount, p.currency, p.status, p.plan_id, pl.name AS plan_name, ps.term_months, ps.payment_method, ps.expires_at, p.created_at").
Joins("LEFT JOIN plan AS pl ON pl.id = p.plan_id").
Joins("LEFT JOIN plan_subscriptions AS ps ON ps.payment_id = p.id").
Where("p.user_id = ?", result.UserID).
Order("p.created_at DESC").
Scan(&rows).Error; err != nil {
s.logger.Error("Failed to fetch payment history", "error", err)
return nil, status.Error(codes.Internal, "Failed to fetch payment history")
}
items := make([]paymentapi.PaymentHistoryItem, 0, len(rows))
for _, row := range rows {
items = append(items, paymentapi.PaymentHistoryItem{
ID: row.ID,
Amount: row.Amount,
Currency: normalizeCurrency(row.Currency),
Status: normalizePaymentStatus(row.Status),
PlanID: row.PlanID,
PlanName: row.PlanName,
InvoiceID: buildInvoiceID(row.ID),
Kind: paymentKindSubscription,
TermMonths: row.TermMonths,
PaymentMethod: normalizeOptionalPaymentMethod(row.PaymentMethod),
ExpiresAt: row.ExpiresAt,
CreatedAt: row.CreatedAt,
})
}
var topups []model.WalletTransaction
if err := s.db.WithContext(ctx).
Where("user_id = ? AND type = ? AND payment_id IS NULL", result.UserID, walletTransactionTypeTopup).
Order("created_at DESC").
Find(&topups).Error; err != nil {
s.logger.Error("Failed to fetch wallet topups", "error", err)
return nil, status.Error(codes.Internal, "Failed to fetch payment history")
}
for _, topup := range topups {
createdAt := topup.CreatedAt
items = append(items, paymentapi.PaymentHistoryItem{
ID: topup.ID,
Amount: topup.Amount,
Currency: normalizeCurrency(topup.Currency),
Status: "success",
InvoiceID: buildInvoiceID(topup.ID),
Kind: paymentKindWalletTopup,
CreatedAt: createdAt,
})
}
sort.Slice(items, func(i, j int) bool {
left := time.Time{}
right := time.Time{}
if items[i].CreatedAt != nil {
left = *items[i].CreatedAt
}
if items[j].CreatedAt != nil {
right = *items[j].CreatedAt
}
return right.After(left)
})
payload := make([]*appv1.PaymentHistoryItem, 0, len(items))
for _, item := range items {
copyItem := item
payload = append(payload, toProtoPaymentHistoryItem(&copyItem))
}
return &appv1.ListPaymentHistoryResponse{Payments: payload}, nil
}
func (s *appServices) TopupWallet(ctx context.Context, req *appv1.TopupWalletRequest) (*appv1.TopupWalletResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
amount := req.GetAmount()
if amount < 1 {
return nil, status.Error(codes.InvalidArgument, "Amount must be at least 1")
}
transaction := &model.WalletTransaction{
ID: uuid.New().String(),
UserID: result.UserID,
Type: walletTransactionTypeTopup,
Amount: amount,
Currency: model.StringPtr("USD"),
Note: model.StringPtr(fmt.Sprintf("Wallet top-up of %.2f USD", amount)),
}
notification := &model.Notification{
ID: uuid.New().String(),
UserID: result.UserID,
Type: "billing.topup",
Title: "Wallet credited",
Message: fmt.Sprintf("Your wallet has been credited with %.2f USD.", amount),
Metadata: model.StringPtr(mustMarshalJSON(map[string]interface{}{
"wallet_transaction_id": transaction.ID,
"invoice_id": buildInvoiceID(transaction.ID),
})),
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if _, err := lockUserForUpdate(ctx, tx, result.UserID); err != nil {
return err
}
if err := tx.Create(transaction).Error; err != nil {
return err
}
if err := tx.Create(notification).Error; err != nil {
return err
}
return nil
}); err != nil {
s.logger.Error("Failed to top up wallet", "error", err)
return nil, status.Error(codes.Internal, "Failed to top up wallet")
}
balance, err := model.GetWalletBalance(ctx, s.db, result.UserID)
if err != nil {
s.logger.Error("Failed to calculate wallet balance", "error", err)
return nil, status.Error(codes.Internal, "Failed to top up wallet")
}
return &appv1.TopupWalletResponse{
WalletTransaction: toProtoWalletTransaction(transaction),
WalletBalance: balance,
InvoiceId: buildInvoiceID(transaction.ID),
}, nil
}
func (s *appServices) DownloadInvoice(ctx context.Context, req *appv1.DownloadInvoiceRequest) (*appv1.DownloadInvoiceResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Invoice not found")
}
paymentRecord, err := query.Payment.WithContext(ctx).
Where(query.Payment.ID.Eq(id), query.Payment.UserID.Eq(result.UserID)).
First()
if err == nil {
invoiceText, filename, buildErr := s.buildPaymentInvoice(ctx, paymentRecord)
if buildErr != nil {
s.logger.Error("Failed to build payment invoice", "error", buildErr)
return nil, status.Error(codes.Internal, "Failed to download invoice")
}
return &appv1.DownloadInvoiceResponse{
Filename: filename,
ContentType: "text/plain; charset=utf-8",
Content: invoiceText,
}, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Error("Failed to load payment invoice", "error", err)
return nil, status.Error(codes.Internal, "Failed to download invoice")
}
var topup model.WalletTransaction
if err := s.db.WithContext(ctx).
Where("id = ? AND user_id = ? AND type = ? AND payment_id IS NULL", id, result.UserID, walletTransactionTypeTopup).
First(&topup).Error; err == nil {
return &appv1.DownloadInvoiceResponse{
Filename: buildInvoiceFilename(topup.ID),
ContentType: "text/plain; charset=utf-8",
Content: buildTopupInvoice(&topup),
}, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Error("Failed to load topup invoice", "error", err)
return nil, status.Error(codes.Internal, "Failed to download invoice")
}
return nil, status.Error(codes.NotFound, "Invoice not found")
}

View File

@@ -0,0 +1,390 @@
package app
import (
"context"
"errors"
"strings"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"stream.api/internal/database/model"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func (s *appServices) ListNotifications(ctx context.Context, _ *appv1.ListNotificationsRequest) (*appv1.ListNotificationsResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
var rows []model.Notification
if err := s.db.WithContext(ctx).
Where("user_id = ?", result.UserID).
Order("created_at DESC").
Find(&rows).Error; err != nil {
s.logger.Error("Failed to list notifications", "error", err)
return nil, status.Error(codes.Internal, "Failed to load notifications")
}
items := make([]*appv1.Notification, 0, len(rows))
for _, row := range rows {
items = append(items, toProtoNotification(row))
}
return &appv1.ListNotificationsResponse{Notifications: items}, nil
}
func (s *appServices) MarkNotificationRead(ctx context.Context, req *appv1.MarkNotificationReadRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Notification not found")
}
res := s.db.WithContext(ctx).
Model(&model.Notification{}).
Where("id = ? AND user_id = ?", id, result.UserID).
Update("is_read", true)
if res.Error != nil {
s.logger.Error("Failed to update notification", "error", res.Error)
return nil, status.Error(codes.Internal, "Failed to update notification")
}
if res.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "Notification not found")
}
return messageResponse("Notification updated"), nil
}
func (s *appServices) MarkAllNotificationsRead(ctx context.Context, _ *appv1.MarkAllNotificationsReadRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
if err := s.db.WithContext(ctx).
Model(&model.Notification{}).
Where("user_id = ? AND is_read = ?", result.UserID, false).
Update("is_read", true).Error; err != nil {
s.logger.Error("Failed to mark all notifications as read", "error", err)
return nil, status.Error(codes.Internal, "Failed to update notifications")
}
return messageResponse("All notifications marked as read"), nil
}
func (s *appServices) DeleteNotification(ctx context.Context, req *appv1.DeleteNotificationRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Notification not found")
}
res := s.db.WithContext(ctx).
Where("id = ? AND user_id = ?", id, result.UserID).
Delete(&model.Notification{})
if res.Error != nil {
s.logger.Error("Failed to delete notification", "error", res.Error)
return nil, status.Error(codes.Internal, "Failed to delete notification")
}
if res.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "Notification not found")
}
return messageResponse("Notification deleted"), nil
}
func (s *appServices) ClearNotifications(ctx context.Context, _ *appv1.ClearNotificationsRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
if err := s.db.WithContext(ctx).Where("user_id = ?", result.UserID).Delete(&model.Notification{}).Error; err != nil {
s.logger.Error("Failed to clear notifications", "error", err)
return nil, status.Error(codes.Internal, "Failed to clear notifications")
}
return messageResponse("All notifications deleted"), nil
}
func (s *appServices) ListDomains(ctx context.Context, _ *appv1.ListDomainsRequest) (*appv1.ListDomainsResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
var rows []model.Domain
if err := s.db.WithContext(ctx).
Where("user_id = ?", result.UserID).
Order("created_at DESC").
Find(&rows).Error; err != nil {
s.logger.Error("Failed to list domains", "error", err)
return nil, status.Error(codes.Internal, "Failed to load domains")
}
items := make([]*appv1.Domain, 0, len(rows))
for _, row := range rows {
item := row
items = append(items, toProtoDomain(&item))
}
return &appv1.ListDomainsResponse{Domains: items}, nil
}
func (s *appServices) CreateDomain(ctx context.Context, req *appv1.CreateDomainRequest) (*appv1.CreateDomainResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
name := normalizeDomain(req.GetName())
if name == "" || !strings.Contains(name, ".") || strings.ContainsAny(name, "/ ") {
return nil, status.Error(codes.InvalidArgument, "Invalid domain")
}
var count int64
if err := s.db.WithContext(ctx).
Model(&model.Domain{}).
Where("user_id = ? AND name = ?", result.UserID, name).
Count(&count).Error; err != nil {
s.logger.Error("Failed to validate domain", "error", err)
return nil, status.Error(codes.Internal, "Failed to create domain")
}
if count > 0 {
return nil, status.Error(codes.InvalidArgument, "Domain already exists")
}
item := &model.Domain{
ID: uuid.New().String(),
UserID: result.UserID,
Name: name,
}
if err := s.db.WithContext(ctx).Create(item).Error; err != nil {
s.logger.Error("Failed to create domain", "error", err)
return nil, status.Error(codes.Internal, "Failed to create domain")
}
return &appv1.CreateDomainResponse{Domain: toProtoDomain(item)}, nil
}
func (s *appServices) DeleteDomain(ctx context.Context, req *appv1.DeleteDomainRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Domain not found")
}
res := s.db.WithContext(ctx).
Where("id = ? AND user_id = ?", id, result.UserID).
Delete(&model.Domain{})
if res.Error != nil {
s.logger.Error("Failed to delete domain", "error", res.Error)
return nil, status.Error(codes.Internal, "Failed to delete domain")
}
if res.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "Domain not found")
}
return messageResponse("Domain deleted"), nil
}
func (s *appServices) ListAdTemplates(ctx context.Context, _ *appv1.ListAdTemplatesRequest) (*appv1.ListAdTemplatesResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
var items []model.AdTemplate
if err := s.db.WithContext(ctx).
Where("user_id = ?", result.UserID).
Order("is_default DESC").
Order("created_at DESC").
Find(&items).Error; err != nil {
s.logger.Error("Failed to list ad templates", "error", err)
return nil, status.Error(codes.Internal, "Failed to load ad templates")
}
payload := make([]*appv1.AdTemplate, 0, len(items))
for _, item := range items {
copyItem := item
payload = append(payload, toProtoAdTemplate(&copyItem))
}
return &appv1.ListAdTemplatesResponse{Templates: payload}, nil
}
func (s *appServices) CreateAdTemplate(ctx context.Context, req *appv1.CreateAdTemplateRequest) (*appv1.CreateAdTemplateResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
if err := ensurePaidPlan(result.User); err != nil {
return nil, err
}
name := strings.TrimSpace(req.GetName())
vastURL := strings.TrimSpace(req.GetVastTagUrl())
if name == "" || vastURL == "" {
return nil, status.Error(codes.InvalidArgument, "Name and VAST URL are required")
}
format := normalizeAdFormat(req.GetAdFormat())
if format == "mid-roll" && (req.Duration == nil || *req.Duration <= 0) {
return nil, status.Error(codes.InvalidArgument, "Duration is required for mid-roll templates")
}
item := &model.AdTemplate{
ID: uuid.New().String(),
UserID: result.UserID,
Name: name,
Description: nullableTrimmedString(req.Description),
VastTagURL: vastURL,
AdFormat: model.StringPtr(format),
Duration: int32PtrToInt64Ptr(req.Duration),
IsActive: model.BoolPtr(req.IsActive == nil || *req.IsActive),
IsDefault: req.IsDefault != nil && *req.IsDefault,
}
if !adTemplateIsActive(item.IsActive) {
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetDefaultTemplates(tx, result.UserID, ""); err != nil {
return err
}
}
return tx.Create(item).Error
}); err != nil {
s.logger.Error("Failed to create ad template", "error", err)
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
return &appv1.CreateAdTemplateResponse{Template: toProtoAdTemplate(item)}, nil
}
func (s *appServices) UpdateAdTemplate(ctx context.Context, req *appv1.UpdateAdTemplateRequest) (*appv1.UpdateAdTemplateResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
if err := ensurePaidPlan(result.User); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
name := strings.TrimSpace(req.GetName())
vastURL := strings.TrimSpace(req.GetVastTagUrl())
if name == "" || vastURL == "" {
return nil, status.Error(codes.InvalidArgument, "Name and VAST URL are required")
}
format := normalizeAdFormat(req.GetAdFormat())
if format == "mid-roll" && (req.Duration == nil || *req.Duration <= 0) {
return nil, status.Error(codes.InvalidArgument, "Duration is required for mid-roll templates")
}
var item model.AdTemplate
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, result.UserID).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
s.logger.Error("Failed to load ad template", "error", err)
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
item.Name = name
item.Description = nullableTrimmedString(req.Description)
item.VastTagURL = vastURL
item.AdFormat = model.StringPtr(format)
item.Duration = int32PtrToInt64Ptr(req.Duration)
if req.IsActive != nil {
item.IsActive = model.BoolPtr(*req.IsActive)
}
if req.IsDefault != nil {
item.IsDefault = *req.IsDefault
}
if !adTemplateIsActive(item.IsActive) {
item.IsDefault = false
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetDefaultTemplates(tx, result.UserID, item.ID); err != nil {
return err
}
}
return tx.Save(&item).Error
}); err != nil {
s.logger.Error("Failed to update ad template", "error", err)
return nil, status.Error(codes.Internal, "Failed to save ad template")
}
return &appv1.UpdateAdTemplateResponse{Template: toProtoAdTemplate(&item)}, nil
}
func (s *appServices) DeleteAdTemplate(ctx context.Context, req *appv1.DeleteAdTemplateRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
if err := ensurePaidPlan(result.User); err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("ad_template_id = ? AND user_id = ?", id, result.UserID).
Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
res := tx.Where("id = ? AND user_id = ?", id, result.UserID).Delete(&model.AdTemplate{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Ad template not found")
}
s.logger.Error("Failed to delete ad template", "error", err)
return nil, status.Error(codes.Internal, "Failed to delete ad template")
}
return messageResponse("Ad template deleted"), nil
}
func (s *appServices) ListPlans(ctx context.Context, _ *appv1.ListPlansRequest) (*appv1.ListPlansResponse, error) {
if _, err := s.authenticate(ctx); err != nil {
return nil, err
}
var plans []model.Plan
if err := s.db.WithContext(ctx).Where("is_active = ?", true).Find(&plans).Error; err != nil {
s.logger.Error("Failed to fetch plans", "error", err)
return nil, status.Error(codes.Internal, "Failed to fetch plans")
}
items := make([]*appv1.Plan, 0, len(plans))
for _, plan := range plans {
copyPlan := plan
items = append(items, toProtoPlan(&copyPlan))
}
return &appv1.ListPlansResponse{Plans: items}, nil
}

View File

@@ -0,0 +1,274 @@
package app
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"stream.api/internal/database/model"
appv1 "stream.api/internal/gen/proto/app/v1"
)
func (s *appServices) GetUploadUrl(ctx context.Context, req *appv1.GetUploadUrlRequest) (*appv1.GetUploadUrlResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
if s.storageProvider == nil {
return nil, status.Error(codes.FailedPrecondition, "Storage provider is not configured")
}
filename := strings.TrimSpace(req.GetFilename())
if filename == "" {
return nil, status.Error(codes.InvalidArgument, "Filename is required")
}
fileID := uuid.New().String()
key := fmt.Sprintf("videos/%s/%s-%s", result.UserID, fileID, filename)
uploadURL, err := s.storageProvider.GeneratePresignedURL(key, 15*time.Minute)
if err != nil {
s.logger.Error("Failed to generate upload URL", "error", err)
return nil, status.Error(codes.Internal, "Storage error")
}
return &appv1.GetUploadUrlResponse{UploadUrl: uploadURL, Key: key, FileId: fileID}, nil
}
func (s *appServices) CreateVideo(ctx context.Context, req *appv1.CreateVideoRequest) (*appv1.CreateVideoResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
title := strings.TrimSpace(req.GetTitle())
if title == "" {
return nil, status.Error(codes.InvalidArgument, "Title is required")
}
videoURL := strings.TrimSpace(req.GetUrl())
if videoURL == "" {
return nil, status.Error(codes.InvalidArgument, "URL is required")
}
statusValue := "ready"
processingStatus := "READY"
storageType := detectStorageType(videoURL)
description := strings.TrimSpace(req.GetDescription())
format := strings.TrimSpace(req.GetFormat())
video := &model.Video{
ID: uuid.New().String(),
UserID: result.UserID,
Name: title,
Title: title,
Description: nullableTrimmedString(&description),
URL: videoURL,
Size: req.GetSize(),
Duration: req.GetDuration(),
Format: format,
Status: &statusValue,
ProcessingStatus: &processingStatus,
StorageType: &storageType,
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(video).Error; err != nil {
return err
}
return tx.Model(&model.User{}).
Where("id = ?", result.UserID).
UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error
}); err != nil {
s.logger.Error("Failed to create video", "error", err)
return nil, status.Error(codes.Internal, "Failed to create video")
}
return &appv1.CreateVideoResponse{Video: toProtoVideo(video)}, nil
}
func (s *appServices) ListVideos(ctx context.Context, req *appv1.ListVideosRequest) (*appv1.ListVideosResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
page := req.GetPage()
if page < 1 {
page = 1
}
limit := req.GetLimit()
if limit <= 0 {
limit = 10
}
if limit > 100 {
limit = 100
}
offset := int((page - 1) * limit)
db := s.db.WithContext(ctx).Model(&model.Video{}).Where("user_id = ?", result.UserID)
if search := strings.TrimSpace(req.GetSearch()); search != "" {
like := "%" + search + "%"
db = db.Where("title ILIKE ? OR description ILIKE ?", like, like)
}
if st := strings.TrimSpace(req.GetStatus()); st != "" && !strings.EqualFold(st, "all") {
db = db.Where("status = ?", normalizeVideoStatusValue(st))
}
var total int64
if err := db.Count(&total).Error; err != nil {
s.logger.Error("Failed to count videos", "error", err)
return nil, status.Error(codes.Internal, "Failed to fetch videos")
}
var videos []model.Video
if err := db.Order("created_at DESC").Offset(offset).Limit(int(limit)).Find(&videos).Error; err != nil {
s.logger.Error("Failed to list videos", "error", err)
return nil, status.Error(codes.Internal, "Failed to fetch videos")
}
items := make([]*appv1.Video, 0, len(videos))
for i := range videos {
items = append(items, toProtoVideo(&videos[i]))
}
return &appv1.ListVideosResponse{Videos: items, Total: total, Page: page, Limit: limit}, nil
}
func (s *appServices) GetVideo(ctx context.Context, req *appv1.GetVideoRequest) (*appv1.GetVideoResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Video not found")
}
_ = s.db.WithContext(ctx).Model(&model.Video{}).
Where("id = ? AND user_id = ?", id, result.UserID).
UpdateColumn("views", gorm.Expr("views + ?", 1)).Error
var video model.Video
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, result.UserID).First(&video).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Video not found")
}
s.logger.Error("Failed to fetch video", "error", err)
return nil, status.Error(codes.Internal, "Failed to fetch video")
}
return &appv1.GetVideoResponse{Video: toProtoVideo(&video)}, nil
}
func (s *appServices) UpdateVideo(ctx context.Context, req *appv1.UpdateVideoRequest) (*appv1.UpdateVideoResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Video not found")
}
updates := map[string]any{}
if title := strings.TrimSpace(req.GetTitle()); title != "" {
updates["name"] = title
updates["title"] = title
}
if req.Description != nil {
desc := strings.TrimSpace(req.GetDescription())
updates["description"] = nullableTrimmedString(&desc)
}
if urlValue := strings.TrimSpace(req.GetUrl()); urlValue != "" {
updates["url"] = urlValue
}
if req.Size > 0 {
updates["size"] = req.GetSize()
}
if req.Duration > 0 {
updates["duration"] = req.GetDuration()
}
if req.Format != nil {
updates["format"] = strings.TrimSpace(req.GetFormat())
}
if req.Status != nil {
updates["status"] = normalizeVideoStatusValue(req.GetStatus())
}
if len(updates) == 0 {
return nil, status.Error(codes.InvalidArgument, "No changes provided")
}
res := s.db.WithContext(ctx).
Model(&model.Video{}).
Where("id = ? AND user_id = ?", id, result.UserID).
Updates(updates)
if res.Error != nil {
s.logger.Error("Failed to update video", "error", res.Error)
return nil, status.Error(codes.Internal, "Failed to update video")
}
if res.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "Video not found")
}
var video model.Video
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, result.UserID).First(&video).Error; err != nil {
s.logger.Error("Failed to reload video", "error", err)
return nil, status.Error(codes.Internal, "Failed to update video")
}
return &appv1.UpdateVideoResponse{Video: toProtoVideo(&video)}, nil
}
func (s *appServices) DeleteVideo(ctx context.Context, req *appv1.DeleteVideoRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Video not found")
}
var video model.Video
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, result.UserID).First(&video).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Video not found")
}
s.logger.Error("Failed to load video", "error", err)
return nil, status.Error(codes.Internal, "Failed to delete video")
}
if s.storageProvider != nil && shouldDeleteStoredObject(video.URL) {
if err := s.storageProvider.Delete(video.URL); err != nil {
if parsedKey := extractObjectKey(video.URL); parsedKey != "" && parsedKey != video.URL {
if deleteErr := s.storageProvider.Delete(parsedKey); deleteErr != nil {
s.logger.Error("Failed to delete video object", "error", deleteErr, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to delete video")
}
} else {
s.logger.Error("Failed to delete video object", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to delete video")
}
}
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("video_id = ? AND user_id = ?", video.ID, result.UserID).Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
if err := tx.Where("id = ? AND user_id = ?", video.ID, result.UserID).Delete(&model.Video{}).Error; err != nil {
return err
}
return tx.Model(&model.User{}).
Where("id = ?", result.UserID).
UpdateColumn("storage_used", gorm.Expr("storage_used - ?", video.Size)).Error
}); err != nil {
s.logger.Error("Failed to delete video", "error", err)
return nil, status.Error(codes.Internal, "Failed to delete video")
}
return messageResponse("Video deleted successfully"), nil
}

View File

@@ -0,0 +1,174 @@
package redis
import (
"context"
"encoding/json"
"fmt"
"time"
goredis "github.com/redis/go-redis/v9"
"stream.api/internal/video/runtime/domain"
)
const (
JobQueueKey = "render:jobs:queue"
LogChannel = "render:jobs:logs"
ResourceChannel = "render:agents:resources"
JobUpdateChannel = "render:jobs:updates"
)
type Adapter struct{ client *goredis.Client }
func NewAdapter(addr, password string, db int) (*Adapter, error) {
client := goredis.NewClient(&goredis.Options{Addr: addr, Password: password, DB: db})
if err := client.Ping(context.Background()).Err(); err != nil {
return nil, err
}
return &Adapter{client: client}, nil
}
func (r *Adapter) Client() *goredis.Client { return r.client }
func (r *Adapter) Enqueue(ctx context.Context, job *domain.Job) error {
data, err := json.Marshal(job)
if err != nil {
return err
}
timestamp := time.Now().UnixNano()
score := float64(-(int64(job.Priority) * 1000000000) - timestamp)
return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: data}).Err()
}
func (r *Adapter) Dequeue(ctx context.Context) (*domain.Job, error) {
for {
if ctx.Err() != nil {
return nil, ctx.Err()
}
res, err := r.client.ZPopMin(ctx, JobQueueKey, 1).Result()
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
return nil, err
}
if len(res) == 0 {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(time.Second):
continue
}
}
var raw []byte
switch member := res[0].Member.(type) {
case string:
raw = []byte(member)
case []byte:
raw = member
default:
return nil, fmt.Errorf("unexpected redis queue payload type %T", member)
}
var job domain.Job
if err := json.Unmarshal(raw, &job); err != nil {
return nil, err
}
return &job, nil
}
}
func (r *Adapter) Publish(ctx context.Context, jobID string, logLine string, progress float64) error {
payload, err := json.Marshal(domain.LogEntry{JobID: jobID, Line: logLine, Progress: progress})
if err != nil {
return err
}
return r.client.Publish(ctx, LogChannel, payload).Err()
}
func (r *Adapter) Subscribe(ctx context.Context, jobID string) (<-chan domain.LogEntry, error) {
pubsub := r.client.Subscribe(ctx, LogChannel)
ch := make(chan domain.LogEntry)
go func() {
defer close(ch)
defer pubsub.Close()
for msg := range pubsub.Channel() {
var entry domain.LogEntry
if err := json.Unmarshal([]byte(msg.Payload), &entry); err != nil {
continue
}
if jobID == "" || entry.JobID == jobID {
ch <- entry
}
}
}()
return ch, nil
}
func (r *Adapter) PublishResource(ctx context.Context, agentID string, data []byte) error {
var decoded struct {
CPU float64 `json:"cpu"`
RAM float64 `json:"ram"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
payload, err := json.Marshal(domain.SystemResource{AgentID: agentID, CPU: decoded.CPU, RAM: decoded.RAM})
if err != nil {
return err
}
return r.client.Publish(ctx, ResourceChannel, payload).Err()
}
func (r *Adapter) SubscribeResources(ctx context.Context) (<-chan domain.SystemResource, error) {
pubsub := r.client.Subscribe(ctx, ResourceChannel)
ch := make(chan domain.SystemResource)
go func() {
defer close(ch)
defer pubsub.Close()
for msg := range pubsub.Channel() {
var entry domain.SystemResource
if err := json.Unmarshal([]byte(msg.Payload), &entry); err != nil {
continue
}
ch <- entry
}
}()
return ch, nil
}
func (r *Adapter) PublishCancel(ctx context.Context, agentID string, jobID string) error {
return r.client.Publish(ctx, fmt.Sprintf("render:agents:%s:cancel", agentID), jobID).Err()
}
func (r *Adapter) SubscribeCancel(ctx context.Context, agentID string) (<-chan string, error) {
pubsub := r.client.Subscribe(ctx, fmt.Sprintf("render:agents:%s:cancel", agentID))
ch := make(chan string)
go func() {
defer close(ch)
defer pubsub.Close()
for msg := range pubsub.Channel() {
ch <- msg.Payload
}
}()
return ch, nil
}
func (r *Adapter) PublishJobUpdate(ctx context.Context, jobID string, status string) error {
payload, err := json.Marshal(map[string]string{"job_id": jobID, "status": status})
if err != nil {
return err
}
return r.client.Publish(ctx, JobUpdateChannel, payload).Err()
}
func (r *Adapter) SubscribeJobUpdates(ctx context.Context) (<-chan string, error) {
pubsub := r.client.Subscribe(ctx, JobUpdateChannel)
ch := make(chan string)
go func() {
defer close(ch)
defer pubsub.Close()
for msg := range pubsub.Channel() {
ch <- msg.Payload
}
}()
return ch, nil
}

View File

@@ -0,0 +1,26 @@
package domain
import "time"
type AgentStatus string
const (
AgentStatusOnline AgentStatus = "online"
AgentStatusOffline AgentStatus = "offline"
AgentStatusBusy AgentStatus = "busy"
)
type Agent struct {
ID string `json:"id"`
Name string `json:"name"`
Platform string `json:"platform"`
Backend string `json:"backend"`
Version string `json:"version"`
Capacity int32 `json:"capacity"`
Status AgentStatus `json:"status"`
CPU float64 `json:"cpu"`
RAM float64 `json:"ram"`
LastHeartbeat time.Time `json:"last_heartbeat"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@@ -0,0 +1,37 @@
package domain
import "time"
type JobStatus string
const (
JobStatusPending JobStatus = "pending"
JobStatusRunning JobStatus = "running"
JobStatusSuccess JobStatus = "success"
JobStatusFailure JobStatus = "failure"
JobStatusCancelled JobStatus = "cancelled"
)
type Job struct {
ID string `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
Status JobStatus `json:"status" gorm:"type:varchar(32);index"`
Priority int `json:"priority" gorm:"default:0;index"`
UserID string `json:"user_id" gorm:"index"`
Name string `json:"name"`
TimeLimit int64 `json:"time_limit"`
InputURL string `json:"input_url"`
OutputURL string `json:"output_url"`
TotalDuration int64 `json:"total_duration"`
CurrentTime int64 `json:"current_time"`
Progress float64 `json:"progress"`
AgentID *string `json:"agent_id" gorm:"type:uuid;index"`
Logs string `json:"logs" gorm:"type:text"`
Config string `json:"config" gorm:"type:text"`
Cancelled bool `json:"cancelled" gorm:"default:false"`
RetryCount int `json:"retry_count" gorm:"default:0"`
MaxRetries int `json:"max_retries" gorm:"default:3"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Job) TableName() string { return "render_jobs" }

View File

@@ -0,0 +1,13 @@
package domain
type LogEntry struct {
JobID string `json:"job_id"`
Line string `json:"line"`
Progress float64 `json:"progress"`
}
type SystemResource struct {
AgentID string `json:"agent_id"`
CPU float64 `json:"cpu"`
RAM float64 `json:"ram"`
}

View File

@@ -0,0 +1,111 @@
package grpc
import (
"sync"
"time"
"stream.api/internal/video/runtime/domain"
)
type AgentInfo struct {
ID string
Name string
Platform string
Backend string
Version string
Capacity int32
CPU float64
RAM float64
LastHeartbeat time.Time
ConnectedAt time.Time
CommandCh chan string
}
type AgentManager struct {
mu sync.RWMutex
agents map[string]*AgentInfo
}
func NewAgentManager() *AgentManager {
return &AgentManager{agents: make(map[string]*AgentInfo)}
}
func (am *AgentManager) Register(id string, name, platform, backend, version string, capacity int32) {
am.mu.Lock()
defer am.mu.Unlock()
now := time.Now()
if existing, ok := am.agents[id]; ok {
existing.Name = name
existing.Platform = platform
existing.Backend = backend
existing.Version = version
existing.Capacity = capacity
existing.LastHeartbeat = now
return
}
am.agents[id] = &AgentInfo{ID: id, Name: name, Platform: platform, Backend: backend, Version: version, Capacity: capacity, LastHeartbeat: now, ConnectedAt: now, CommandCh: make(chan string, 10)}
}
func (am *AgentManager) GetCommandChannel(id string) (chan string, bool) {
am.mu.RLock()
defer am.mu.RUnlock()
agent, ok := am.agents[id]
if !ok {
return nil, false
}
return agent.CommandCh, true
}
func (am *AgentManager) SendCommand(id string, cmd string) bool {
am.mu.RLock()
defer am.mu.RUnlock()
agent, ok := am.agents[id]
if !ok {
return false
}
select {
case agent.CommandCh <- cmd:
return true
default:
return false
}
}
func (am *AgentManager) UpdateHeartbeat(id string) {
am.mu.Lock()
defer am.mu.Unlock()
if agent, ok := am.agents[id]; ok {
agent.LastHeartbeat = time.Now()
}
}
func (am *AgentManager) UpdateResources(id string, cpu, ram float64) {
am.mu.Lock()
defer am.mu.Unlock()
if agent, ok := am.agents[id]; ok {
agent.CPU = cpu
agent.RAM = ram
agent.LastHeartbeat = time.Now()
}
}
func (am *AgentManager) Unregister(id string) {
am.mu.Lock()
defer am.mu.Unlock()
delete(am.agents, id)
}
func (am *AgentManager) ListAll() []*domain.Agent {
am.mu.RLock()
defer am.mu.RUnlock()
now := time.Now()
all := make([]*domain.Agent, 0, len(am.agents))
for _, info := range am.agents {
status := domain.AgentStatusOnline
if now.Sub(info.LastHeartbeat) >= 60*time.Second {
status = domain.AgentStatusOffline
}
all = append(all, &domain.Agent{ID: info.ID, Name: info.Name, Platform: info.Platform, Backend: info.Backend, Version: info.Version, Capacity: info.Capacity, Status: status, CPU: info.CPU, RAM: info.RAM, LastHeartbeat: info.LastHeartbeat, CreatedAt: info.ConnectedAt, UpdatedAt: info.LastHeartbeat})
}
return all
}

View File

@@ -0,0 +1,361 @@
package grpc
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"sync"
"time"
grpcpkg "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"stream.api/internal/video/runtime/domain"
"stream.api/internal/video/runtime/proto"
"stream.api/internal/video/runtime/services"
)
type Server struct {
proto.UnimplementedWoodpeckerServer
proto.UnimplementedWoodpeckerAuthServer
jobService *services.JobService
agentManager *AgentManager
agentSecret string
sessions sync.Map
agentJobs sync.Map
onAgentEvent func(string, *services.AgentWithStats)
}
func NewServer(jobService *services.JobService, agentSecret string) *Server {
return &Server{jobService: jobService, agentManager: NewAgentManager(), agentSecret: agentSecret}
}
func (s *Server) SetAgentEventHandler(handler func(string, *services.AgentWithStats)) {
s.onAgentEvent = handler
}
func (s *Server) Register(grpcServer grpcpkg.ServiceRegistrar) {
proto.RegisterWoodpeckerServer(grpcServer, s)
proto.RegisterWoodpeckerAuthServer(grpcServer, s)
}
func (s *Server) SendCommand(agentID string, cmd string) bool {
return s.agentManager.SendCommand(agentID, cmd)
}
func (s *Server) ListAgents() []*domain.Agent { return s.agentManager.ListAll() }
func (s *Server) ListAgentsWithStats() []*services.AgentWithStats {
agents := s.agentManager.ListAll()
result := make([]*services.AgentWithStats, 0, len(agents))
for _, agent := range agents {
result = append(result, &services.AgentWithStats{Agent: agent, ActiveJobCount: int64(len(s.getAgentJobs(agent.ID)))})
}
return result
}
func (s *Server) getAgentWithStats(agentID string) *services.AgentWithStats {
for _, agent := range s.ListAgentsWithStats() {
if agent != nil && agent.Agent != nil && agent.ID == agentID {
return agent
}
}
return nil
}
func (s *Server) Version(context.Context, *proto.Empty) (*proto.VersionResponse, error) {
return &proto.VersionResponse{GrpcVersion: 15, ServerVersion: "stream.api"}, nil
}
func generateToken() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func generateAgentID() string {
return strconv.FormatInt(time.Now().UnixNano(), 10)
}
func (s *Server) getAgentIDFromContext(ctx context.Context) (string, string, bool) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", "", false
}
tokens := md.Get("token")
if len(tokens) == 0 {
return "", "", false
}
token := tokens[0]
if id, ok := s.sessions.Load(token); ok {
return id.(string), token, true
}
return "", "", false
}
func (s *Server) Next(context.Context, *proto.NextRequest) (*proto.NextResponse, error) {
return nil, status.Error(codes.Unimplemented, "use StreamJobs")
}
func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreamingServer[proto.Workflow]) error {
ctx := stream.Context()
agentID, _, ok := s.getAgentIDFromContext(ctx)
if !ok {
return status.Error(codes.Unauthenticated, "invalid or missing token")
}
s.agentManager.UpdateHeartbeat(agentID)
cancelCh, _ := s.jobService.SubscribeCancel(ctx, agentID)
commandCh, _ := s.agentManager.GetCommandChannel(agentID)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case cmd := <-commandCh:
payload, _ := json.Marshal(map[string]any{"image": "alpine", "commands": []string{"echo 'System Command'"}, "environment": map[string]string{}, "action": cmd})
if err := stream.Send(&proto.Workflow{Id: fmt.Sprintf("cmd-%s-%d", agentID, time.Now().UnixNano()), Timeout: 300, Payload: payload}); err != nil {
return err
}
case jobID := <-cancelCh:
if s.isJobAssigned(agentID, jobID) {
if err := stream.Send(&proto.Workflow{Id: jobID, Cancel: true}); err != nil {
return err
}
}
case <-ctx.Done():
return nil
case <-ticker.C:
s.agentManager.UpdateHeartbeat(agentID)
jobCtx, cancel := context.WithTimeout(ctx, time.Second)
job, err := s.jobService.GetNextJob(jobCtx)
cancel()
if err != nil || job == nil {
continue
}
s.trackJobAssignment(agentID, job.ID)
if err := s.jobService.AssignJob(ctx, job.ID, agentID); err != nil {
s.untrackJobAssignment(agentID, job.ID)
continue
}
var config map[string]any
if err := json.Unmarshal([]byte(job.Config), &config); err != nil {
_ = s.jobService.UpdateJobStatus(ctx, job.ID, domain.JobStatusFailure)
s.untrackJobAssignment(agentID, job.ID)
continue
}
image, _ := config["image"].(string)
if image == "" {
image = "alpine"
}
commands := []string{"echo 'No commands specified'"}
if raw, ok := config["commands"].([]any); ok && len(raw) > 0 {
commands = commands[:0]
for _, item := range raw {
if text, ok := item.(string); ok {
commands = append(commands, text)
}
}
if len(commands) == 0 {
commands = []string{"echo 'No commands specified'"}
}
}
payload, _ := json.Marshal(map[string]any{"image": image, "commands": commands, "environment": map[string]string{}})
if err := stream.Send(&proto.Workflow{Id: job.ID, Timeout: job.TimeLimit, Payload: payload}); err != nil {
_ = s.jobService.UpdateJobStatus(ctx, job.ID, domain.JobStatusPending)
s.untrackJobAssignment(agentID, job.ID)
return err
}
}
}
}
func (s *Server) SubmitStatus(stream grpcpkg.ClientStreamingServer[proto.StatusUpdate, proto.Empty]) error {
ctx := stream.Context()
agentID, _, ok := s.getAgentIDFromContext(ctx)
if !ok {
return status.Error(codes.Unauthenticated, "invalid or missing token")
}
for {
update, err := stream.Recv()
if err != nil {
return stream.SendAndClose(&proto.Empty{})
}
switch update.Type {
case 0, 1:
_ = s.jobService.ProcessLog(ctx, update.StepUuid, update.Data)
case 4:
var progress float64
fmt.Sscanf(string(update.Data), "%f", &progress)
_ = s.jobService.UpdateJobProgress(ctx, update.StepUuid, progress)
case 5:
var stats struct {
CPU float64 `json:"cpu"`
RAM float64 `json:"ram"`
}
if json.Unmarshal(update.Data, &stats) == nil {
s.agentManager.UpdateResources(agentID, stats.CPU, stats.RAM)
if s.onAgentEvent != nil {
s.onAgentEvent("agent_update", s.getAgentWithStats(agentID))
}
}
_ = s.jobService.PublishSystemResources(ctx, agentID, update.Data)
}
}
}
func (s *Server) Init(ctx context.Context, req *proto.InitRequest) (*proto.Empty, error) {
if err := s.jobService.UpdateJobStatus(ctx, req.Id, domain.JobStatusRunning); err != nil {
return nil, status.Error(codes.Internal, "failed to update job status")
}
return &proto.Empty{}, nil
}
func (s *Server) Wait(context.Context, *proto.WaitRequest) (*proto.WaitResponse, error) {
return &proto.WaitResponse{Canceled: false}, nil
}
func (s *Server) Done(ctx context.Context, req *proto.DoneRequest) (*proto.Empty, error) {
agentID, _, ok := s.getAgentIDFromContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
jobStatus := domain.JobStatusSuccess
if req.State != nil && req.State.Error != "" {
jobStatus = domain.JobStatusFailure
}
if err := s.jobService.UpdateJobStatus(ctx, req.Id, jobStatus); err != nil {
return nil, status.Error(codes.Internal, "failed to update job status")
}
s.untrackJobAssignment(agentID, req.Id)
return &proto.Empty{}, nil
}
func (s *Server) Update(context.Context, *proto.UpdateRequest) (*proto.Empty, error) {
return &proto.Empty{}, nil
}
func (s *Server) Log(ctx context.Context, req *proto.LogRequest) (*proto.Empty, error) {
if _, _, ok := s.getAgentIDFromContext(ctx); !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
for _, entry := range req.LogEntries {
if entry.StepUuid != "" {
_ = s.jobService.ProcessLog(ctx, entry.StepUuid, entry.Data)
}
}
return &proto.Empty{}, nil
}
func (s *Server) Extend(context.Context, *proto.ExtendRequest) (*proto.Empty, error) {
return &proto.Empty{}, nil
}
func (s *Server) RegisterAgent(ctx context.Context, req *proto.RegisterAgentRequest) (*proto.RegisterAgentResponse, error) {
if req.Info == nil {
return nil, status.Error(codes.InvalidArgument, "connection info is required")
}
id, _, ok := s.getAgentIDFromContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
hostname := ""
if req.Info.CustomLabels != nil {
hostname = req.Info.CustomLabels["hostname"]
}
name := hostname
if name == "" {
name = fmt.Sprintf("agent-%s", id)
}
s.agentManager.Register(id, name, req.Info.Platform, req.Info.Backend, req.Info.Version, req.Info.Capacity)
if s.onAgentEvent != nil {
s.onAgentEvent("agent_update", s.getAgentWithStats(id))
}
return &proto.RegisterAgentResponse{AgentId: id}, nil
}
func (s *Server) UnregisterAgent(ctx context.Context, _ *proto.Empty) (*proto.Empty, error) {
agentID, token, ok := s.getAgentIDFromContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
for _, jobID := range s.getAgentJobs(agentID) {
_ = s.jobService.UpdateJobStatus(ctx, jobID, domain.JobStatusFailure)
s.untrackJobAssignment(agentID, jobID)
}
s.sessions.Delete(token)
s.agentJobs.Delete(agentID)
agent := s.getAgentWithStats(agentID)
s.agentManager.Unregister(agentID)
if s.onAgentEvent != nil {
s.onAgentEvent("agent_update", agent)
}
return &proto.Empty{}, nil
}
func (s *Server) ReportHealth(ctx context.Context, _ *proto.ReportHealthRequest) (*proto.Empty, error) {
agentID, _, ok := s.getAgentIDFromContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
s.agentManager.UpdateHeartbeat(agentID)
return &proto.Empty{}, nil
}
func (s *Server) Auth(ctx context.Context, req *proto.AuthRequest) (*proto.AuthResponse, error) {
if s.agentSecret != "" && req.AgentToken != s.agentSecret {
return nil, status.Error(codes.Unauthenticated, "invalid agent secret")
}
agentID := req.AgentId
if len(agentID) > 6 && agentID[:6] == "agent-" {
agentID = agentID[6:]
}
if agentID == "" {
agentID = generateAgentID()
}
accessToken := generateToken()
s.sessions.Store(accessToken, agentID)
return &proto.AuthResponse{Status: "ok", AgentId: agentID, AccessToken: accessToken}, nil
}
func (s *Server) trackJobAssignment(agentID, jobID string) {
jobSetInterface, _ := s.agentJobs.LoadOrStore(agentID, &sync.Map{})
if jobSet, ok := jobSetInterface.(*sync.Map); ok {
jobSet.Store(jobID, true)
}
}
func (s *Server) untrackJobAssignment(agentID, jobID string) {
if jobSetInterface, ok := s.agentJobs.Load(agentID); ok {
if jobSet, ok := jobSetInterface.(*sync.Map); ok {
jobSet.Delete(jobID)
}
}
}
func (s *Server) isJobAssigned(agentID, jobID string) bool {
if jobSetInterface, ok := s.agentJobs.Load(agentID); ok {
if jobSet, ok := jobSetInterface.(*sync.Map); ok {
_, found := jobSet.Load(jobID)
return found
}
}
return false
}
func (s *Server) getAgentJobs(agentID string) []string {
jobs := []string{}
if jobSetInterface, ok := s.agentJobs.Load(agentID); ok {
if jobSet, ok := jobSetInterface.(*sync.Map); ok {
jobSet.Range(func(key, _ any) bool {
if jobID, ok := key.(string); ok {
jobs = append(jobs, jobID)
}
return true
})
}
}
return jobs
}

Some files were not shown because too many files have changed in this diff Show More