diff --git a/api/admin.go b/api/admin.go new file mode 100644 index 0000000..0605588 --- /dev/null +++ b/api/admin.go @@ -0,0 +1,34 @@ +package api + +import ( + "net/http" + + "git.sp4ke.com/sp4ke/bit4sat/storage" + "git.sp4ke.com/sp4ke/bit4sat/utils" + "github.com/gin-gonic/gin" +) + +func adminGetInfo(c *gin.Context) { + + adminToken := c.Param("adminToken") + + // Get upload payments + //upInfo, err := storage.GetUploadInfoByToken(adminToken) + //if err != nil { + //utils.JSONErrPriv(c, http.StatusInternalServerError, err) + //return + //} + + // Sum of payments + paySum, err := storage.GetMsatBalanceByAdminToken(adminToken) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "msat_avail": paySum, + }) + + return +} diff --git a/api/download.go b/api/download.go index 9897097..9a744a4 100644 --- a/api/download.go +++ b/api/download.go @@ -43,7 +43,16 @@ func download(c *gin.Context) { return } - dlSess := session.Values["session-id"] + dlSessVal := session.Values["session-id"] + var dlSess string + + if dlSess, ok = dlSessVal.(string); dlSessVal != nil && !ok { + log.Printf("error parsing session dlSessVal") + utils.JSONErr(c, http.StatusExpectationFailed, + "start a new download session session") + return + } + dlIdVal := session.Values["dlId"] var sessDlId string @@ -66,7 +75,7 @@ func download(c *gin.Context) { // If the stored dlId is different than the // current dl id it means we're downloading a different // file - if dlSess == nil || (sessDlId != dlId) { + if dlSessVal == nil || (sessDlId != dlId) { // This is a new download session log.Println("new download session") @@ -182,23 +191,35 @@ func download(c *gin.Context) { return } - // If invoice paid send the files + // If invoice paid create an authorized download link linked to session + // The link will expire after after one hour + // Also bump up the expirey of the DlSession to one hour if invoice.Settled { log.Println("dl invoice paid, sending") - // set response type to zip mime type - c.Header("Content-Type", "application/zip") - c.Header("Content-Disposition", "attachment; filename=download.zip") - - err := storage.ZipFiles(upFilesMeta, c.Writer) + downKey, err := storage.GetShortId() if err != nil { - log.Fatal(err) + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + key := fmt.Sprintf("session_%s_download_key_%s", dlSess, downKey) + // We store the upload id in the value to use it when retreiving + // files + db.SetKeyVal(key, upId) + db.ExpireKey(key, MaxAgeDlKeySession) + + // Session will also expire in 1 hour + session.Options.MaxAge = MaxAgeDlKeySession + err = session.Save(c.Request, c.Writer) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return } - //c.JSON(http.StatusOK, gin.H{ - //"download": "ok", - //"invoice": invoice, - //}) + c.JSON(http.StatusOK, gin.H{ + "download_key": downKey, + "invoice": invoice, + }) return } @@ -211,6 +232,77 @@ func download(c *gin.Context) { return } +func getFiles(c *gin.Context) { + downKey := c.Param("dlKey") + + // Get the session + session, err := SessionStore.Get(c.Request, DlSessionKey) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + dlSessVal := session.Values["session-id"] + + var dlSess string + var ok bool + + if dlSess, ok = dlSessVal.(string); dlSessVal != nil && !ok { + log.Printf("error parsing session dlSessVal") + utils.JSONErr(c, http.StatusExpectationFailed, + "user session lost, start a new download session session") + return + } + + if dlSessVal == nil { + utils.JSONErr(c, http.StatusGone, "the download session has expired") + return + } + + key := fmt.Sprintf("session_%s_download_key_%s", dlSess, downKey) + + // Check if download key exists in db + exists, err := db.Exists(key) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + if !exists { + utils.JSONErr(c, http.StatusGone, "the download session has expired") + return + } + + // We should now allow the user to get the files + + // We retreive the files from the upId stored on the session download key + var upId string + err = db.GetFromKey(key, &upId) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + // Get files metadata for upload + var upFilesMeta []storage.FileUpload + upFilesMeta, err = storage.GetUploadFilesMeta(upId) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + c.Header("Content-Type", "application/zip") + c.Header("Content-Disposition", "attachment; filename=download.zip") + + err = storage.ZipFiles(upFilesMeta, c.Writer) + if err != nil { + utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return + } + + return +} + func TestDownHandler(c *gin.Context) { sess := sessions.Default(c) diff --git a/api/handlers.go b/api/handlers.go index ab37d75..20d01ac 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -119,19 +119,20 @@ func pollInvoice(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "invoice": invoice, }) + return case err := <-errorChan: utils.JSONErrPriv(c, http.StatusInternalServerError, err) + return } // We stopped waiting for the invoice, it must have expired c.JSON(http.StatusGone, gin.H{ "invoice": invoice, }) + return } - - return } // Was used by ln-charge @@ -152,7 +153,7 @@ func invoiceCbHandler(c *gin.Context) { // get upload id related to invoice var uploadId string - invoiceUploadKey := fmt.Sprintf("invoice_%s_uploadid", invoice.RHash) + invoiceUploadKey := fmt.Sprintf("invoice_%s_upload_id", invoice.RHash) err := db.DB.Redis.Do(radix.FlatCmd(&uploadId, "GET", invoiceUploadKey)) if err != nil { diff --git a/api/routes.go b/api/routes.go index aa914b4..e42eb03 100644 --- a/api/routes.go +++ b/api/routes.go @@ -39,8 +39,16 @@ func (api *API) Run() { downRoute := v1.Group("/d") { - // Download - downRoute.GET(":dlId", download) + // Query download + downRoute.GET("/q/:dlId", download) + + // Get file + downRoute.GET("/g/:dlKey", getFiles) + } + + adminRoute := v1.Group("/a") + { + adminRoute.GET("/info/:adminToken", adminGetInfo) } // test rout diff --git a/api/session.go b/api/session.go index 1041e18..d8dca3c 100644 --- a/api/session.go +++ b/api/session.go @@ -3,8 +3,9 @@ package api import "github.com/boj/redistore" const ( - UpSessionKey = "bit4sat-up" - DlSessionKey = "bit4sat-dl" + UpSessionKey = "bit4sat-up" + DlSessionKey = "bit4sat-dl" + MaxAgeDlKeySession = 3600 // max age for a paid and authorized download key ) var ( diff --git a/api/upload_ctrl.go b/api/upload_ctrl.go index 76736bf..b01de8b 100644 --- a/api/upload_ctrl.go +++ b/api/upload_ctrl.go @@ -65,7 +65,7 @@ func (ctrl UploadCtrl) New(c *gin.Context) { utils.JSONErrPriv(c, http.StatusInternalServerError, err) return } - up.DownloadID = dlId + up.DownloadId = dlId err = up.TxWrite(tx) diff --git a/db/redis_utils.go b/db/redis_utils.go index 577fe92..8bcc2e9 100644 --- a/db/redis_utils.go +++ b/db/redis_utils.go @@ -11,3 +11,14 @@ func GetFromKey(key string, target interface{}) error { func SetKeyVal(key string, val interface{}) error { return DB.Redis.Do(radix.FlatCmd(nil, "SET", key, val)) } + +func ExpireKey(key string, seconds int) error { + return DB.Redis.Do(radix.FlatCmd(nil, "EXPIRE", key, seconds)) +} + +func Exists(key string) (bool, error) { + var exists bool + err := DB.Redis.Do(radix.Cmd(&exists, "EXISTS", key)) + + return exists, err +} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ca97439..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,100 +0,0 @@ -version: "3.4" - -volumes: - redis-db: - postgresql: - file-storage: - gocache: - #sqlite: - #maria-conf: - -services: - api: - image: sp4ke/bit4sat - container_name: bit4sat-api - build: - context: . - dockerfile: ./docker/Dockerfile - - environment: - - GO111MODULE=on - - API_HOST=bit4sat-api - - LND_GRPC_HOST=lnd - - LND_GRPC_PORT=10009 - - BIT4SAT_STORAGE_PATH=/storage - - GOPATH=/go - - SQL_DB_HOST=postgres - - SQL_DB_USER=bit4sat - - SQL_DB_PASS=bit4sat - - LN_CHARGE_API=ln-charge-test:9112 - - LN_CHARGE_TOKEN=3emU3Fy8VasHCzMaMXHSVJYpQSqH3yXQj8N5cQFBbq3botrudJuR7zQkBBmFSbAmgXs9GD4j4U3J4R2sMfgqPo8q - - SESSION_SECRET=Ai7fCy36UE5cb9wcmdAxxRXwYyQDsDMr6rYocA6Eava7pdiB29EusLbb9sTYWS1e - - GRPC_SSL_CIPHER_SUITES="HIGH+ECDSA" - - SHORT_ID_SALT=Czp6NtlGpt0ebzG1DuUND1nMftLUR77c - - ENV=dev - - # Used in case of ssl problems - - HTTP_PROXY=http://tinyproxy:8888 - - #deploy: - #replicas: 1 - # - ports: - - "8880:8880" - - depends_on: - - redis - - postgres - - volumes: - - $PWD:/src - - gocache:/go - #- /fastData/go:/go - #- ./db-storage:/sqlite - - file-storage:/storage - - working_dir: /src - - networks: - - btc-test-overlay - - btc-overlay - - default - - #maria: - #image: mariadb:latest - #environment: - #- MYSQL_ROOT_PASSWORD=pass - #- MYSQL_DATABASE=bit4sat - #- MYSQL_USER=bit4sat - #- MYSQL_PASSWORD=bit4sat - - #volumes: - #- db:/var/lib/mysql - #- maria-conf:/etc/mysql - postgres: - image: postgres:11.2 - environment: - - POSTGRES_PASSWORD=bit4sat - - POSTGRES_USER=bit4sat - - POSTGRES_DB=bit4sat - - volumes: - - postgresql:/var/lib/postgresql/data - - - - redis: - image: redis:alpine - - volumes: - - redis-db:/data - - command: - - redis-server - - --appendonly yes - -networks: - btc-test-overlay: - external: true - btc-overlay: - external: true diff --git a/ln/invoice.go b/ln/invoice.go index e0eee11..a1e375a 100644 --- a/ln/invoice.go +++ b/ln/invoice.go @@ -142,6 +142,7 @@ func InvoiceFromLndIn(lndInvoice *lnrpc.Invoice) *Invoice { RHash: hex.EncodeToString(lndInvoice.RHash), PreImage: hex.EncodeToString(lndInvoice.RPreimage), CreatedAt: timestamp(time.Unix(lndInvoice.CreationDate, 0)), + PaidAt: timestamp(time.Unix(lndInvoice.SettleDate, 0)), Status: InvoiceStatus[UnPaid], } diff --git a/storage/upload_model.go b/storage/upload_model.go index a9c5f7c..3d12794 100644 --- a/storage/upload_model.go +++ b/storage/upload_model.go @@ -234,7 +234,7 @@ type FileUpload struct { type Upload struct { UploadId string `db:"upload_id"` - DownloadID string `db:"download_id"` + DownloadId string `db:"download_id"` AskFee bool `db:"ask_fee"` Created time.Time `db:"created"` AskCurrency string `db:"ask_currency"` @@ -242,7 +242,18 @@ type Upload struct { InvoiceRhash string `db:"invoice_rhash"` // used as id Settled bool `db:"settled"` UploadStatus uint32 `db:"status"` // upload flag status - AdminToekn string `db:"admin_token"` + AdminToken string `db:"admin_token"` +} + +type UploadPayments struct { + PaymentId string `db:"payment_id_rhash"` + MsatAmount float64 `db:"msat_amount"` + PayCreated time.Time `db:"payment_created"` +} + +type UploadInfo struct { + Upload + UploadPayments } func (u Upload) MarshalBinary() ([]byte, error) { @@ -358,7 +369,7 @@ func GetUploadInvoice(uploadId string) (*ln.Invoice, error) { return nil, err } - log.Printf("GetUploadInvoice returned %#v") + //log.Printf("GetUploadInvoice returned %#v", invoice) return &invoice, nil } @@ -393,6 +404,36 @@ func GetUploadFilesMeta(uploadId string) ([]FileUpload, error) { } +// Get all payments linked to an admin token +func GetUploadPaymentsByToken(adminToken string) ([]*UploadInfo, error) { + upInfo := []*UploadInfo{} + query := `SELECT + uploads.upload_id, + uploads.download_id, + uploads.ask_fee, + uploads.ask_currency, + uploads.ask_amount, + uploads.admin_token, + upload_payments.msat_amount, + upload_payments.created AS payment_created + FROM uploads, upload_payments + WHERE uploads.admin_token = $1 + ` + err := DB.Sql.Select(&upInfo, query, adminToken) + return upInfo, err +} + +func GetMsatBalanceByAdminToken(adminToken string) (float64, error) { + var balance float64 + query := `SELECT SUM(upload_payments.msat_amount) + FROM uploads, upload_payments + WHERE uploads.upload_id = upload_payments.upload_id + AND uploads.admin_token = $1` + + err := DB.Sql.Get(&balance, query, adminToken) + return balance, err +} + func GetUploadById(id string) (*Upload, error) { key := fmt.Sprintf("upload_%s", id) up := Upload{} diff --git a/watchers/invoices.go b/watchers/invoices.go index 79357af..f0b9b62 100644 --- a/watchers/invoices.go +++ b/watchers/invoices.go @@ -127,12 +127,12 @@ func handleSettledInvoice(invoice *lnrpc.Invoice) { log.Printf("error handleSettledInvoice: %s", err) return } - log.Printf("stored invoice %#v", storedInvoice) + //log.Printf("stored invoice %#v", storedInvoice) // Update stored invoice fields newInvoice := ln.UpdateInvoiceFromLnd(storedInvoice, invoice) - log.Printf("new invoice %#v", newInvoice) + //log.Printf("new invoice %#v", newInvoice) // Set invoice for upload err = storage.SetUploadInvoice(uploadId, newInvoice) @@ -164,7 +164,7 @@ func handleSettledInvoice(invoice *lnrpc.Invoice) { // Get upload_id related to this invoice key = fmt.Sprintf("invoice_id_%s_upload_id", storedInvoice.RHash) - log.Printf("looking for %s", key) + //log.Printf("looking for %s", key) var uploadId string err := db.DB.Redis.Do(radix.FlatCmd(&uploadId, "GET", key)) if err != nil { @@ -173,7 +173,7 @@ func handleSettledInvoice(invoice *lnrpc.Invoice) { } // Add payment to the corresponding upload_id (user account) - log.Printf("updating payment for upload %s", uploadId) + //log.Printf("updating payment for upload %s", uploadId) err = storage.AddPaymentToUpload(uploadId, storedInvoice.RHash, storedInvoice.Msatoshi) if err != nil { diff --git a/web/Caddyfile b/web/Caddyfile index 82c61cb..9307f8e 100644 --- a/web/Caddyfile +++ b/web/Caddyfile @@ -20,6 +20,11 @@ rewrite /d { to / } +## Redeem view +rewrite /r { + to / +} + ## test proxy /t localhost:8880 { transparent diff --git a/web/bit4sat_dev_dist.tar b/web/bit4sat_dev_dist.tar new file mode 100644 index 0000000..d0a06ad Binary files /dev/null and b/web/bit4sat_dev_dist.tar differ diff --git a/web/src/AdminView.vue b/web/src/AdminView.vue new file mode 100644 index 0000000..7333116 --- /dev/null +++ b/web/src/AdminView.vue @@ -0,0 +1,32 @@ + + + diff --git a/web/src/App.vue b/web/src/App.vue index 535d0ea..3fcb395 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -15,6 +15,7 @@ const Worker = GetWorker('main'); import { mapState, mapGetters } from 'vuex' const dlUrlRegex = /d\/(\w+)\/?/ +const adminUrlRegex = /r\/(\w+)\/?/ export default { @@ -45,6 +46,15 @@ export default { params: {dlId} }) } + if (loc.pathname.match(adminUrlRegex)) { + let adminToken = adminUrlRegex.exec(loc.pathname)[1] + history.replaceState("", `admin`, '/' ) + this.$router.replace({ + name: 'admin', + replace: true, + params: {adminToken} + }) + } }, } diff --git a/web/src/DownloadView.vue b/web/src/DownloadView.vue index 0831de1..5e6448e 100644 --- a/web/src/DownloadView.vue +++ b/web/src/DownloadView.vue @@ -47,31 +47,43 @@ export default { this.$store.commit('setFiles', data.files) Api.pollInvoice(data.invoice.rhash) - .then((res)=>{ - - // invoice paid we can try again to download - if(res.ok){ - //Api.download(self.dlId) - console.log("calling set download") - self.setDownload() - } - }) - - + .then((res)=>{ + // invoice paid we can try again to download + if(res.ok){ + self.fetchDownloadKey() + } }) - .catch((e)=>{console.log(e)}) - // download + }) .catch((e)=>{console.log(e)}) + } else { console.log("calling set download") - self.setDownload() + res.json().then((data)=>{ + console.log(data) + self.setDownload(data.download_key) + }) } }) }, methods: { - setDownload(){ + fetchDownloadKey() { + Api.download(this.dlId) + .then((res)=>{ + if (!res.ok ) { + this.error = true + this.errMsg = "an error occured, try reloading the page" + } else { + res.json().then((data)=>{ + this.setDownload(data.download_key) + }) + } + + + }) + }, + setDownload(downloadKey){ this.showDl = true; - this.dlLink = Api.endPoints.download + '/' + this.dlId + this.dlLink = Api.endPoints.getFiles + '/' + downloadKey } }, diff --git a/web/src/Pay.vue b/web/src/Pay.vue index d3f92c0..6f4c02b 100644 --- a/web/src/Pay.vue +++ b/web/src/Pay.vue @@ -126,6 +126,7 @@ export default {