github.com/readium/readium-lcp-server@v0.0.0-20240509124024-799e77a0bbd6/frontend/webpurchase/webpurchase.go (about) 1 // Copyright 2020 Readium Foundation. All rights reserved. 2 // Use of this source code is governed by a BSD-style license 3 // that can be found in the LICENSE file exposed on Github (readium) in the project repository. 4 5 package webpurchase 6 7 import ( 8 "bytes" 9 "database/sql" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "log" 14 "net/http" 15 "time" 16 17 "github.com/readium/readium-lcp-server/api" 18 "github.com/readium/readium-lcp-server/config" 19 "github.com/readium/readium-lcp-server/dbutils" 20 "github.com/readium/readium-lcp-server/frontend/webpublication" 21 "github.com/readium/readium-lcp-server/frontend/webuser" 22 "github.com/readium/readium-lcp-server/license" 23 licensestatuses "github.com/readium/readium-lcp-server/license_statuses" 24 uuid "github.com/satori/go.uuid" 25 ) 26 27 // ErrNotFound Error is thrown when a purchase is not found 28 var ErrNotFound = errors.New("purchase not found") 29 30 // ErrNoChange is thrown when an update action does not change any rows (not found) 31 var ErrNoChange = errors.New("no lines were updated") 32 33 // WebPurchase defines possible interactions with the db 34 type WebPurchase interface { 35 Get(id int64) (Purchase, error) 36 GenerateOrGetLicense(purchase Purchase) (license.License, error) 37 GetPartialLicense(purchase Purchase) (license.License, error) 38 GetLicenseStatusDocument(purchase Purchase) (licensestatuses.LicenseStatus, error) 39 GetByLicenseID(licenseID string) (Purchase, error) 40 List(page int, pageNum int) func() (Purchase, error) 41 ListByUser(userID int64, page int, pageNum int) func() (Purchase, error) 42 Add(p Purchase) error 43 Update(p Purchase) error 44 } 45 46 // Purchase status 47 const ( 48 StatusToBeRenewed string = "to-be-renewed" 49 StatusToBeReturned string = "to-be-returned" 50 StatusError string = "error" 51 StatusOk string = "ok" 52 ) 53 54 // Enumeration of PurchaseType 55 const ( 56 BUY string = "BUY" 57 LOAN string = "LOAN" 58 ) 59 60 // Purchase struct defines a user in json and database 61 // PurchaseType: BUY or LOAN 62 type Purchase struct { 63 ID int64 `json:"id,omitempty"` 64 UUID string `json:"uuid"` 65 Publication webpublication.Publication `json:"publication"` 66 User webuser.User `json:"user"` 67 LicenseUUID *string `json:"licenseUuid,omitempty"` 68 Type string `json:"type"` 69 TransactionDate time.Time `json:"transactionDate,omitempty"` 70 StartDate *time.Time `json:"startDate,omitempty"` 71 EndDate *time.Time `json:"endDate,omitempty"` 72 Status string `json:"status"` 73 MaxEndDate *time.Time `json:"maxEndDate,omitempty"` 74 } 75 76 type PurchaseManager struct { 77 db *sql.DB 78 dbGetByID *sql.Stmt 79 dbGetByLicenseID *sql.Stmt 80 dbList *sql.Stmt 81 dbListByUser *sql.Stmt 82 } 83 84 func convertRecordsToPurchases(rows *sql.Rows) func() (Purchase, error) { 85 86 return func() (Purchase, error) { 87 var err error 88 var purchase Purchase 89 if rows == nil { 90 return purchase, ErrNotFound 91 } 92 if rows.Next() { 93 purchase, err = convertRecordToPurchase(rows) 94 if err != nil { 95 return purchase, err 96 } 97 } else { 98 rows.Close() 99 err = ErrNotFound 100 } 101 return purchase, err 102 } 103 } 104 105 func convertRecordToPurchase(rows *sql.Rows) (Purchase, error) { 106 purchase := Purchase{} 107 user := webuser.User{} 108 pub := webpublication.Publication{} 109 110 err := rows.Scan( 111 &purchase.ID, 112 &purchase.UUID, 113 &purchase.Type, 114 &purchase.TransactionDate, 115 &purchase.LicenseUUID, 116 &purchase.StartDate, 117 &purchase.EndDate, 118 &purchase.Status, 119 &user.ID, 120 &user.UUID, 121 &user.Name, 122 &user.Email, 123 &user.Password, 124 &user.Hint, 125 &pub.ID, 126 &pub.UUID, 127 &pub.Title, 128 &pub.Status) 129 130 if err != nil { 131 return Purchase{}, err 132 } 133 134 // Load relations 135 purchase.User = user 136 purchase.Publication = pub 137 return purchase, nil 138 } 139 140 // Get a purchase using its id 141 func (pManager PurchaseManager) Get(id int64) (Purchase, error) { 142 143 // note: does not use QueryRow here, as converRecordToPurchase used in converRecordsToPurchase. 144 rows, err := pManager.dbGetByID.Query(id) 145 if err != nil { 146 return Purchase{}, err 147 } 148 defer rows.Close() 149 150 if rows.Next() { 151 purchase, err := convertRecordToPurchase(rows) 152 if err != nil { 153 return Purchase{}, err 154 } 155 156 if purchase.LicenseUUID != nil { 157 // Query LSD Server to retrieve max end date (PotentialRights.End) 158 // FIXME: calling the lsd server at this point is too heavy: the max end date should be in the db. 159 statusDocument, err := pManager.GetLicenseStatusDocument(purchase) 160 161 if err != nil { 162 return Purchase{}, err 163 } 164 165 if statusDocument.PotentialRights != nil && statusDocument.PotentialRights.End != nil && !(*statusDocument.PotentialRights.End).IsZero() { 166 purchase.MaxEndDate = statusDocument.PotentialRights.End 167 } 168 } 169 return purchase, nil 170 } 171 return Purchase{}, ErrNotFound 172 } 173 174 // GetByLicenseID gets a purchase by the associated license id 175 func (pManager PurchaseManager) GetByLicenseID(licenseID string) (Purchase, error) { 176 177 // note: does not use QueryRow here, as convertRecordToPurchase used in converRecordsToPurchase. 178 rows, err := pManager.dbGetByLicenseID.Query(licenseID) 179 if err != nil { 180 return Purchase{}, err 181 } 182 defer rows.Close() 183 184 if rows.Next() { 185 return convertRecordToPurchase(rows) 186 // FIXME: difference with Get(), we don't retrieve the potential end date from the LSD server 187 } 188 return Purchase{}, ErrNotFound 189 } 190 191 // GenerateOrGetLicense generates a new license associated with a purchase, 192 // or gets an existing license, 193 // depending on the value of the license id in the purchase. 194 func (pManager PurchaseManager) GenerateOrGetLicense(purchase Purchase) (license.License, error) { 195 // create a partial license 196 partialLicense := license.License{} 197 198 // set the mandatory provider URI 199 if config.Config.FrontendServer.ProviderUri == "" { 200 return license.License{}, errors.New("mandatory provider URI missing in the configuration") 201 } 202 partialLicense.Provider = config.Config.FrontendServer.ProviderUri 203 204 // get user info from the purchase info 205 encryptedAttrs := []string{"email", "name"} 206 partialLicense.User.Email = purchase.User.Email 207 partialLicense.User.Name = purchase.User.Name 208 partialLicense.User.ID = purchase.User.UUID 209 partialLicense.User.Encrypted = encryptedAttrs 210 211 // get the hashed passphrase from the purchase 212 userKeyValue, err := hex.DecodeString(purchase.User.Password) 213 214 if err != nil { 215 return license.License{}, err 216 } 217 218 userKey := license.UserKey{} 219 userKey.Algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" 220 userKey.Hint = purchase.User.Hint 221 userKey.Value = userKeyValue 222 partialLicense.Encryption.UserKey = userKey 223 224 // In case of a creation of license, add the user rights 225 var copy, print int32 226 if purchase.LicenseUUID == nil { 227 // in case of undefined conf values for copy and print rights, 228 // these rights will be set to zero 229 copy = config.Config.FrontendServer.RightCopy 230 print = config.Config.FrontendServer.RightPrint 231 userRights := license.UserRights{} 232 userRights.Copy = © 233 userRights.Print = &print 234 235 // if this is a loan, include start and end dates from the purchase info 236 if purchase.Type == LOAN { 237 userRights.Start = purchase.StartDate 238 userRights.End = purchase.EndDate 239 } 240 partialLicense.Rights = &userRights 241 } 242 243 // encode in json 244 jsonBody, err := json.Marshal(partialLicense) 245 if err != nil { 246 return license.License{}, err 247 } 248 249 // get the url of the lcp server 250 lcpServerConfig := config.Config.LcpServer 251 var lcpURL string 252 253 if purchase.LicenseUUID == nil { 254 // if the purchase contains no license id, generate a new license 255 lcpURL = lcpServerConfig.PublicBaseUrl + "/contents/" + purchase.Publication.UUID + "/license" 256 } else { 257 // if the purchase contains a license id, fetch an existing license 258 // note: this will not update the license rights 259 lcpURL = lcpServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID 260 } 261 // message to the console 262 log.Println("POST " + lcpURL) 263 264 // add the partial license to the POST request 265 req, err := http.NewRequest("POST", lcpURL, bytes.NewReader(jsonBody)) 266 if err != nil { 267 return license.License{}, err 268 } 269 lcpUpdateAuth := config.Config.LcpUpdateAuth 270 if config.Config.LcpUpdateAuth.Username != "" { 271 req.SetBasicAuth(lcpUpdateAuth.Username, lcpUpdateAuth.Password) 272 } 273 // the body is a partial license in json format 274 req.Header.Add("Content-Type", api.ContentType_LCP_JSON) 275 276 var lcpClient = &http.Client{ 277 Timeout: time.Second * 10, 278 } 279 // POST the request 280 resp, err := lcpClient.Do(req) 281 if err != nil { 282 return license.License{}, err 283 } 284 285 defer resp.Body.Close() 286 287 // if the status code from the request to the lcp server 288 // is neither 201 Created or 200 ok, return an internal error 289 if (purchase.LicenseUUID == nil && resp.StatusCode != 201) || 290 (purchase.LicenseUUID != nil && resp.StatusCode != 200) { 291 return license.License{}, errors.New("the License Server returned an error") 292 } 293 294 // decode the full license 295 fullLicense := license.License{} 296 dec := json.NewDecoder(resp.Body) 297 err = dec.Decode(&fullLicense) 298 299 if err != nil { 300 return license.License{}, errors.New("unable to decode license") 301 } 302 303 // store the license id if it was not already set 304 if purchase.LicenseUUID == nil { 305 purchase.LicenseUUID = &fullLicense.ID 306 pManager.Update(purchase) 307 if err != nil { 308 return license.License{}, errors.New("unable to update the license id") 309 } 310 } 311 312 return fullLicense, nil 313 } 314 315 // GetPartialLicense gets the license associated with a purchase, from the license server 316 func (pManager PurchaseManager) GetPartialLicense(purchase Purchase) (license.License, error) { 317 318 if purchase.LicenseUUID == nil { 319 return license.License{}, errors.New("no license has been yet delivered") 320 } 321 322 lcpServerConfig := config.Config.LcpServer 323 lcpURL := lcpServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID 324 // message to the console 325 log.Println("GET " + lcpURL) 326 // prepare the request 327 req, err := http.NewRequest("GET", lcpURL, nil) 328 if err != nil { 329 return license.License{}, err 330 } 331 // set credentials 332 lcpUpdateAuth := config.Config.LcpUpdateAuth 333 if config.Config.LcpUpdateAuth.Username != "" { 334 req.SetBasicAuth(lcpUpdateAuth.Username, lcpUpdateAuth.Password) 335 } 336 // send the request 337 var lcpClient = &http.Client{ 338 Timeout: time.Second * 10, 339 } 340 resp, err := lcpClient.Do(req) 341 if err != nil { 342 return license.License{}, err 343 } 344 defer resp.Body.Close() 345 346 // the call must return 206 (partial content) because there is no input partial license 347 if resp.StatusCode != 206 { 348 // bad status code 349 return license.License{}, errors.New("the License Server returned an error") 350 } 351 // decode the license 352 partialLicense := license.License{} 353 dec := json.NewDecoder(resp.Body) 354 err = dec.Decode(&partialLicense) 355 356 if err != nil { 357 return license.License{}, errors.New("unable to decode the license") 358 } 359 360 return partialLicense, nil 361 } 362 363 // GetLicenseStatusDocument gets a license status document associated with a purchase 364 func (pManager PurchaseManager) GetLicenseStatusDocument(purchase Purchase) (licensestatuses.LicenseStatus, error) { 365 if purchase.LicenseUUID == nil { 366 return licensestatuses.LicenseStatus{}, errors.New("no license has been yet delivered") 367 } 368 369 lsdServerConfig := config.Config.LsdServer 370 lsdURL := lsdServerConfig.PublicBaseUrl + "/licenses/" + *purchase.LicenseUUID + "/status" 371 log.Println("GET " + lsdURL) 372 req, err := http.NewRequest("GET", lsdURL, nil) 373 if err != nil { 374 return licensestatuses.LicenseStatus{}, err 375 } 376 req.Header.Add("Content-Type", api.ContentType_JSON) 377 378 var lsdClient = &http.Client{ 379 Timeout: time.Second * 10, 380 } 381 382 resp, err := lsdClient.Do(req) 383 if err != nil { 384 return licensestatuses.LicenseStatus{}, err 385 } 386 387 if resp.StatusCode != 200 { 388 // Bad status code 389 return licensestatuses.LicenseStatus{}, errors.New("the License Status Document server returned an error") 390 } 391 392 // Decode status document 393 statusDocument := licensestatuses.LicenseStatus{} 394 dec := json.NewDecoder(resp.Body) 395 err = dec.Decode(&statusDocument) 396 397 if err != nil { 398 return licensestatuses.LicenseStatus{}, err 399 } 400 401 defer resp.Body.Close() 402 403 return statusDocument, nil 404 } 405 406 // List all purchases, with pagination 407 func (pManager PurchaseManager) List(page int, pageNum int) func() (Purchase, error) { 408 409 var rows *sql.Rows 410 var err error 411 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 412 if driver == "mssql" { 413 rows, err = pManager.dbList.Query(pageNum*page, page) 414 } else { 415 rows, err = pManager.dbList.Query(page, pageNum*page) 416 } 417 if err != nil { 418 log.Printf("Failed to get the full list of purchases: %s", err.Error()) 419 } 420 return convertRecordsToPurchases(rows) 421 } 422 423 // ListByUser lists the purchases of a given user, with pagination 424 func (pManager PurchaseManager) ListByUser(userID int64, page int, pageNum int) func() (Purchase, error) { 425 426 var rows *sql.Rows 427 var err error 428 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 429 if driver == "mssql" { 430 rows, err = pManager.dbListByUser.Query(userID, pageNum*page, page) 431 } else { 432 rows, err = pManager.dbListByUser.Query(userID, page, pageNum*page) 433 } 434 if err != nil { 435 log.Printf("Failed to get the user list of purchases: %s", err.Error()) 436 } 437 return convertRecordsToPurchases(rows) 438 } 439 440 // Add a purchase 441 func (pManager PurchaseManager) Add(p Purchase) error { 442 443 // Fill default values 444 if p.TransactionDate.IsZero() { 445 p.TransactionDate = time.Now().UTC().Truncate(time.Second) 446 } 447 448 if p.Type == LOAN && p.StartDate == nil { 449 p.StartDate = &p.TransactionDate 450 } 451 452 // Create uuid 453 uid, err_u := uuid.NewV4() 454 if err_u != nil { 455 return err_u 456 } 457 p.UUID = uid.String() 458 459 _, err := pManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, `INSERT INTO purchase 460 (uuid, publication_id, user_id, type, transaction_date, start_date, end_date, status) 461 VALUES (?, ?, ?, ?, ?, ?, ?, 'ok')`), 462 p.UUID, p.Publication.ID, p.User.ID, string(p.Type), p.TransactionDate, p.StartDate, p.EndDate) 463 464 return err 465 } 466 467 // Update modifies a purchase on a renew or return request 468 // parameters: a Purchase structure withID, LicenseUUID, StartDate, EndDate, Status 469 // EndDate may be undefined (nil), in which case the lsd server will choose the renew period 470 func (pManager PurchaseManager) Update(p Purchase) error { 471 // Get the original purchase from the db 472 origPurchase, err := pManager.Get(p.ID) 473 474 if err != nil { 475 return ErrNotFound 476 } 477 if origPurchase.Status != StatusOk { 478 return errors.New("cannot update an invalid purchase") 479 } 480 if p.Status == StatusToBeRenewed || 481 p.Status == StatusToBeReturned { 482 483 if p.LicenseUUID == nil { 484 return errors.New("cannot return or renew a purchase when no license has been delivered") 485 } 486 487 lsdServerConfig := config.Config.LsdServer 488 lsdURL := lsdServerConfig.PublicBaseUrl + "/licenses/" + *p.LicenseUUID 489 490 if p.Status == StatusToBeRenewed { 491 lsdURL += "/renew" 492 493 if p.EndDate != nil { 494 lsdURL += "?end=" + p.EndDate.Format(time.RFC3339) 495 } 496 497 // Next status if LSD raises no error 498 p.Status = StatusOk 499 } else if p.Status == StatusToBeReturned { 500 lsdURL += "/return" 501 502 // Next status if LSD raises no error 503 p.Status = StatusOk 504 } 505 // message to the console 506 log.Println("PUT " + lsdURL) 507 // prepare the request for renew or return to the license status server 508 req, err := http.NewRequest("PUT", lsdURL, nil) 509 if err != nil { 510 return err 511 } 512 // set credentials 513 lsdAuth := config.Config.LsdNotifyAuth 514 if lsdAuth.Username != "" { 515 req.SetBasicAuth(lsdAuth.Username, lsdAuth.Password) 516 } 517 // call the lsd server 518 var lsdClient = &http.Client{ 519 Timeout: time.Second * 10, 520 } 521 resp, err := lsdClient.Do(req) 522 if err != nil { 523 return err 524 } 525 defer resp.Body.Close() 526 527 // if the renew/return request was refused by the License Status server 528 if resp.StatusCode != 200 { 529 return ErrNoChange 530 } 531 532 // get the new end date from the license server 533 // FIXME: is there a lighter solution to get the new end date? 534 license, err := pManager.GetPartialLicense(origPurchase) 535 if err != nil { 536 return err 537 } 538 p.EndDate = license.Rights.End 539 } else { 540 // status is not "to be renewed" 541 p.Status = StatusOk 542 } 543 // update the db with the updated license id, start date, end date, status 544 result, err := pManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, `UPDATE purchase SET license_uuid=?, start_date=?, end_date=?, status=? WHERE id=?`), 545 p.LicenseUUID, p.StartDate, p.EndDate, p.Status, p.ID) 546 if err != nil { 547 return err 548 } 549 if changed, err := result.RowsAffected(); err == nil { 550 if changed != 1 { 551 return ErrNoChange 552 } 553 } 554 return err 555 } 556 557 // Init initializes the PurchaseManager 558 func Init(db *sql.DB) (i WebPurchase, err error) { 559 560 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 561 562 // if sqlite, create the content table in the frontend db if it does not exist 563 if driver == "sqlite3" { 564 _, err = db.Exec(tableDef) 565 if err != nil { 566 log.Println("Error creating purchase table") 567 return 568 } 569 } 570 571 selectQuery := `SELECT p.id, p.uuid, p.type, p.transaction_date, p.license_uuid, p.start_date, p.end_date, p.status, 572 u.id, u.uuid, u.name, u.email, u.password, u.hint, 573 pu.id, pu.uuid, pu.title, pu.status 574 FROM purchase p JOIN "user" u ON (p.user_id=u.id) JOIN publication pu ON (p.publication_id=pu.id)` 575 576 var dbGetByID *sql.Stmt 577 dbGetByID, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` WHERE p.id = ?`)) 578 if err != nil { 579 return 580 } 581 582 var dbGetByLicenseID *sql.Stmt 583 dbGetByLicenseID, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` WHERE p.license_uuid = ?`)) 584 if err != nil { 585 return 586 } 587 588 var dbList *sql.Stmt 589 if driver == "mssql" { 590 dbList, err = db.Prepare(selectQuery + ` ORDER BY p.transaction_date desc OFFSET ? ROWS FETCH NEXT ? ROWS ONLY`) 591 } else { 592 dbList, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` ORDER BY p.transaction_date desc LIMIT ? OFFSET ?`)) 593 } 594 if err != nil { 595 return 596 } 597 598 var dbListByUser *sql.Stmt 599 if driver == "mssql" { 600 dbListByUser, err = db.Prepare(selectQuery + ` WHERE u.id = ? ORDER BY p.transaction_date desc OFFSET ? ROWS FETCH NEXT ? ROWS ONLY`) 601 } else { 602 dbListByUser, err = db.Prepare(selectQuery + dbutils.GetParamQuery(config.Config.FrontendServer.Database, ` WHERE u.id = ? ORDER BY p.transaction_date desc LIMIT ? OFFSET ?`)) 603 } 604 if err != nil { 605 return 606 } 607 608 i = PurchaseManager{db, dbGetByID, dbGetByLicenseID, dbList, dbListByUser} 609 return 610 } 611 612 const tableDef = "CREATE TABLE IF NOT EXISTS purchase (" + 613 "id integer NOT NULL PRIMARY KEY," + 614 "uuid varchar(255) NOT NULL," + 615 "publication_id integer NOT NULL," + 616 "user_id integer NOT NULL," + 617 "license_uuid varchar(255) NULL," + 618 "type varchar(32) NOT NULL," + 619 "transaction_date datetime," + 620 "start_date datetime," + 621 "end_date datetime," + 622 "status varchar(255) NOT NULL," + 623 "FOREIGN KEY (publication_id) REFERENCES publication(id)," + 624 "FOREIGN KEY (user_id) REFERENCES user(id)" + 625 ");" + 626 "CREATE INDEX IF NOT EXISTS idx_purchase ON purchase (license_uuid)"