diff --git a/.gitignore b/.gitignore
index 4afcff76..3ce6bd97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,13 +4,22 @@ lcpencrypt/lcpencrypt
files/
*.sqlite*
config.yaml
+lcpserver/manage/config.js
.vscode/launch.json
debug
*.exe
*.yaml
-
+**/manage/config.js
+frontend/manage/node_modules/*
+frontend/manage/dist/*
+frontend/manage/js/*
+frontend/manage/js/components/*
+*.map
*.htpasswd
lcpserver/test.sqllite
.DS_Store
+.vscode
lcpencrypt/.DS_Store
lcpserver/.DS_Store
+npm-debug.log
+manage/config.js
\ No newline at end of file
diff --git a/README.md b/README.md
index 1247d29c..e058dc58 100644
--- a/README.md
+++ b/README.md
@@ -151,9 +151,9 @@ NOTE: here is a license section snippet:
```json
license:
links:
- hint: "http://www.edrlab.org/static/hint.html"
- publication: "http://www.edrlab.org/files/{publication_id}"
- status: "http://www.edrlab.org/licenses/{license_id}/status"
+ hint: "http://www.edrlab.org/readiumlcp/static/hint.html"
+ publication: "http://www.edrlab.org/readiumlcp/files/{publication_id}"
+ status: "http://www.edrlab.org/readiumlcp/licenses/{license_id}/status"
```
"license_status": parameters related to the interactions implemented by the License Status server, if any
@@ -174,8 +174,16 @@ NOTE: list files for localization (ex: 'en-US.json, de-DE.json') must match the
- log_directory: point to log file (a .log).
- compliance_tests_mode_on: boolean; if `true`, logging is turned on.
+
+The following CBC / GCM configurable property is DISABLED, see https://github.com/readium/readium-lcp-server/issues/109
"aes256_cbc_or_gcm": either "GCM" or "CBC" (which is the default value). This is used only for encrypting publication resources, not the content key, not the user key check, not the LCP license fields.
+
+Documentation
+============
+Detailed documentation can be found in the [Wiki pages](../../wiki) of the project.
+
+
Contributing
============
Please make a Pull Request with tests at github.com/readium/readium-lcp-server
diff --git a/api/common_server.go b/api/common_server.go
index 446168cf..3d5bc320 100644
--- a/api/common_server.go
+++ b/api/common_server.go
@@ -35,6 +35,7 @@ import (
"github.com/urfave/negroni"
"github.com/jeffbmartinez/delay"
"github.com/technoweenie/grohl"
+ "github.com/rs/cors"
"github.com/readium/readium-lcp-server/problem"
)
@@ -95,12 +96,13 @@ func CreateServerRouter(tplPath string) ServerRouter {
// Does not insert CORS headers as intended, depends on Origin check in the HTTP request...we want the same headers, always.
// IMPORT "github.com/rs/cors"
// //https://github.com/rs/cors#parameters
- // c := cors.New(cors.Options{
- // AllowedOrigins: []string{"*"},
- // AllowedMethods: []string{"POST", "GET", "OPTIONS", "PUT", "DELETE"},
- // Debug: true,
- // })
- // n.Use(c)
+ c := cors.New(cors.Options{
+ AllowedOrigins: []string{"*"},
+ AllowedMethods: []string{"PATCH", "HEAD", "POST", "GET", "OPTIONS", "PUT", "DELETE"},
+ AllowedHeaders: []string{"Range", "Content-Type", "Origin", "X-Requested-With", "Accept", "Accept-Language", "Content-Language", "Authorization"},
+ Debug: true,
+ })
+ n.Use(c)
n.UseHandler(r)
@@ -139,8 +141,10 @@ func ExtraLogger(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)
func CORSHeaders(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
grohl.Log(grohl.Data{"CORS": "yes"})
- rw.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
+ rw.Header().Add("Access-Control-Allow-Methods", "PATCH, HEAD, POST, GET, OPTIONS, PUT, DELETE")
+ rw.Header().Add("Access-Control-Allow-Credentials", "true")
rw.Header().Add("Access-Control-Allow-Origin", "*")
+ rw.Header().Add("Access-Control-Allow-Headers", "Range, Content-Type, Origin, X-Requested-With, Accept, Accept-Language, Content-Language, Authorization")
// before
next(rw, r)
diff --git a/config/config.go b/config/config.go
index a9e3f344..7ebf49b7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -21,7 +21,7 @@
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package config
@@ -35,18 +35,20 @@ import (
)
type Configuration struct {
- Certificate Certificate `yaml:"certificate"`
- Storage Storage `yaml:"storage"`
- License License `yaml:"license"`
- LcpServer ServerInfo `yaml:"lcp"`
- LsdServer ServerInfo `yaml:"lsd"`
- LsdNotifyAuth Auth `yaml:"lsd_notify_auth"`
- LcpUpdateAuth Auth `yaml:"lcp_update_auth"`
- Static Static `yaml:"static"`
- LicenseStatus LicenseStatus `yaml:"license_status"`
- Localization Localization `yaml:"localization"`
- Logging Logging `yaml:"logging"`
- AES256_CBC_OR_GCM string `yaml:"aes256_cbc_or_gcm,omitempty"`
+ Certificate Certificate `yaml:"certificate"`
+ Storage Storage `yaml:"storage"`
+ License License `yaml:"license"`
+ LcpServer ServerInfo `yaml:"lcp"`
+ LsdServer LsdServerInfo `yaml:"lsd"`
+ FrontendServer FrontendServerInfo `yaml:"frontend"`
+ LsdNotifyAuth Auth `yaml:"lsd_notify_auth"`
+ LcpUpdateAuth Auth `yaml:"lcp_update_auth"`
+ LicenseStatus LicenseStatus `yaml:"license_status"`
+ Localization Localization `yaml:"localization"`
+ Logging Logging `yaml:"logging"`
+
+ // DISABLED, see https://github.com/readium/readium-lcp-server/issues/109
+ //AES256_CBC_OR_GCM string `yaml:"aes256_cbc_or_gcm,omitempty"`
}
type ServerInfo struct {
@@ -56,6 +58,19 @@ type ServerInfo struct {
ReadOnly bool `yaml:"readonly,omitempty"`
PublicBaseUrl string `yaml:"public_base_url,omitempty"`
Database string `yaml:"database,omitempty"`
+ Directory string `yaml:"directory,omitempty"`
+}
+
+type LsdServerInfo struct {
+ ServerInfo `yaml:",inline"`
+ LicenseLinkUrl string `yaml:"license_link_url,omitempty"`
+}
+
+type FrontendServerInfo struct {
+ ServerInfo `yaml:",inline"`
+ ProviderID string `yaml:"provider_id"`
+ MasterRepository string `yaml:"master_repository"`
+ EncryptedRepository string `yaml:"encrypted_repository"`
}
type Auth struct {
@@ -72,10 +87,6 @@ type FileSystem struct {
Directory string `yaml:"directory"`
}
-type Static struct {
- Directory string `yaml:"directory"`
-}
-
type Storage struct {
FileSystem FileSystem `yaml:"filesystem"`
AccessId string `yaml:"access_id"`
@@ -123,14 +134,15 @@ func ReadConfig(configFileName string) {
}
err = yaml.Unmarshal(yamlFile, &Config)
+
if err != nil {
panic("Can't unmarshal config. " + configFileName + " -> " + err.Error())
}
}
func SetPublicUrls() error {
- var lcpPublicBaseUrl, lsdPublicBaseUrl, lcpHost, lsdHost string
- var lcpPort, lsdPort int
+ var lcpPublicBaseUrl, lsdPublicBaseUrl, frontendPublicBaseUrl, lcpHost, lsdHost, frontendHost string
+ var lcpPort, lsdPort, frontendPort int
var err error
if lcpHost = Config.LcpServer.Host; lcpHost == "" {
@@ -147,12 +159,22 @@ func SetPublicUrls() error {
}
}
+ if frontendHost = Config.FrontendServer.Host; frontendHost == "" {
+ frontendHost, err = os.Hostname()
+ if err != nil {
+ return err
+ }
+ }
+
if lcpPort = Config.LcpServer.Port; lcpPort == 0 {
lcpPort = 8989
}
if lsdPort = Config.LsdServer.Port; lsdPort == 0 {
lsdPort = 8990
}
+ if frontendPort = Config.FrontendServer.Port; frontendPort == 0 {
+ frontendPort = 80
+ }
if lcpPublicBaseUrl = Config.LcpServer.PublicBaseUrl; lcpPublicBaseUrl == "" {
lcpPublicBaseUrl = "http://" + lcpHost + ":" + strconv.Itoa(lcpPort)
@@ -162,6 +184,10 @@ func SetPublicUrls() error {
lsdPublicBaseUrl = "http://" + lsdHost + ":" + strconv.Itoa(lsdPort)
Config.LsdServer.PublicBaseUrl = lsdPublicBaseUrl
}
+ if frontendPublicBaseUrl = Config.FrontendServer.PublicBaseUrl; frontendPublicBaseUrl == "" {
+ frontendPublicBaseUrl = "http://" + frontendHost + ":" + strconv.Itoa(frontendPort)
+ Config.FrontendServer.PublicBaseUrl = frontendPublicBaseUrl
+ }
return err
}
diff --git a/crypto/aes_cbc.go b/crypto/aes_cbc.go
index 69bf0a8b..40dbdd92 100644
--- a/crypto/aes_cbc.go
+++ b/crypto/aes_cbc.go
@@ -21,7 +21,7 @@
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package crypto
@@ -40,6 +40,7 @@ const (
)
func (e cbcEncrypter) Signature() string {
+ // W3C padding scheme, not PKCS#7 (see last parameter "insertPadLengthAll" [false] of PaddedReader constructor)
return "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
}
@@ -49,7 +50,8 @@ func (e cbcEncrypter) GenerateKey() (ContentKey, error) {
}
func (e cbcEncrypter) Encrypt(key ContentKey, r io.Reader, w io.Writer) error {
- r = PaddedReader(r, aes.BlockSize)
+
+ r = PaddedReader(r, aes.BlockSize, false)
block, err := aes.NewCipher(key)
if err != nil {
@@ -99,7 +101,7 @@ func (c cbcEncrypter) Decrypt(key ContentKey, r io.Reader, w io.Writer) error {
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(buf[aes.BlockSize:], buf[aes.BlockSize:])
- padding := buf[len(buf)-1]
+ padding := buf[len(buf)-1] // padding length valid for both PKCS#7 and W3C schemes
w.Write(buf[aes.BlockSize : len(buf)-int(padding)])
return nil
@@ -107,4 +109,4 @@ func (c cbcEncrypter) Decrypt(key ContentKey, r io.Reader, w io.Writer) error {
func NewAESCBCEncrypter() Encrypter {
return cbcEncrypter(struct{}{})
-}
\ No newline at end of file
+}
diff --git a/crypto/encrypt.go b/crypto/encrypt.go
index 2b72c0b5..6a390e1f 100644
--- a/crypto/encrypt.go
+++ b/crypto/encrypt.go
@@ -28,9 +28,9 @@ package crypto
import (
"crypto/aes"
"io"
-
- "github.com/readium/readium-lcp-server/config"
)
+//"github.com/readium/readium-lcp-server/config"
+// FOR: config.Config.AES256_CBC_OR_GCM
type Encrypter interface {
Encrypt(key ContentKey, r io.Reader, w io.Writer) error
@@ -43,11 +43,15 @@ type Decrypter interface {
}
func NewAESEncrypter_PUBLICATION_RESOURCES() Encrypter {
- if config.Config.AES256_CBC_OR_GCM == "GCM" {
- return NewAESGCMEncrypter()
- } else { // default to CBC
- return NewAESCBCEncrypter()
- }
+
+ return NewAESCBCEncrypter()
+
+ // DISABLED, see https://github.com/readium/readium-lcp-server/issues/109
+ // if config.Config.AES256_CBC_OR_GCM == "GCM" {
+ // return NewAESGCMEncrypter()
+ // } else { // default to CBC
+ // return NewAESCBCEncrypter()
+ // }
}
func NewAESEncrypter_CONTENT_KEY() Encrypter {
diff --git a/crypto/pad.go b/crypto/pad.go
index cb5391b3..fae9b5fa 100644
--- a/crypto/pad.go
+++ b/crypto/pad.go
@@ -27,6 +27,8 @@ package crypto
import (
"io"
+ "math/rand"
+ "time"
)
type paddedReader struct {
@@ -35,6 +37,7 @@ type paddedReader struct {
count byte
left byte
done bool
+ insertPadLengthAll bool
}
func (r *paddedReader) Read(buf []byte) (int, error) {
@@ -73,8 +76,21 @@ func (r *paddedReader) Read(buf []byte) (int, error) {
func (r *paddedReader) pad(buf []byte) (i int, err error) {
capacity := cap(buf)
+
+ src := rand.New(rand.NewSource(time.Now().UnixNano()))
+
for i = 0; capacity > 0 && r.left > 0; i++ {
- buf[i] = r.count
+
+ if (r.insertPadLengthAll) {
+ buf[i] = r.count
+ } else {
+ if r.left == 1 { //capacity == 1 &&
+ buf[i] = r.count
+ } else {
+ buf[i] = byte(src.Intn(254) + 1)
+ }
+ }
+
capacity--
r.left--
}
@@ -86,6 +102,9 @@ func (r *paddedReader) pad(buf []byte) (i int, err error) {
return
}
-func PaddedReader(r io.Reader, blockSize byte) io.Reader {
- return &paddedReader{Reader: r, size: blockSize, count: 0, left: 0, done: false}
+
+// insertPadLengthAll = true means PKCS#7 (padding length inserted in each padding slot),
+// otherwise false means padding length inserted only in the last slot (the rest is random bytes)
+func PaddedReader(r io.Reader, blockSize byte, insertPadLengthAll bool) io.Reader {
+ return &paddedReader{Reader: r, size: blockSize, count: 0, left: 0, done: false, insertPadLengthAll: insertPadLengthAll}
}
diff --git a/crypto/pad_test.go b/crypto/pad_test.go
index 57cd2428..9aaeb41c 100644
--- a/crypto/pad_test.go
+++ b/crypto/pad_test.go
@@ -33,7 +33,7 @@ import (
func TestOneBlock(t *testing.T) {
buf := bytes.NewBufferString("4321")
- reader := PaddedReader(buf, 6)
+ reader := PaddedReader(buf, 6, true)
var out [12]byte
n, err := reader.Read(out[:])
if err != nil && err != io.EOF {
@@ -43,6 +43,8 @@ func TestOneBlock(t *testing.T) {
t.Errorf("should have read 6 bytes, read %d", n)
}
+ // PaddedReader constructor parameter "insertPadLengthAll" is true,
+ // means all last bytes equate the padding length
if out[4] != 2 || out[5] != 2 {
t.Errorf("last values were expected to be 2, got [%x %x]", out[4], out[5])
}
@@ -50,7 +52,7 @@ func TestOneBlock(t *testing.T) {
func TestFullPadding(t *testing.T) {
buf := bytes.NewBufferString("1234")
- reader := PaddedReader(buf, 4)
+ reader := PaddedReader(buf, 4, true)
var out [8]byte
n, err := io.ReadFull(reader, out[:])
@@ -61,14 +63,16 @@ func TestFullPadding(t *testing.T) {
t.Error("should have read 8 bytes, read %d", n)
}
+ // PaddedReader constructor parameter "insertPadLengthAll" is true,
+ // means all last bytes equate the padding length
if out[4] != 4 || out[5] != 4 || out[6] != 4 || out[7] != 4 {
- t.Errorf("last values were expected to be 8, got [%x %x %x %x]", out[4], out[5], out[6], out[7])
+ t.Errorf("last values were expected to be 4, got [%x %x %x %x]", out[4], out[5], out[6], out[7])
}
}
func TestManyBlocks(t *testing.T) {
buf := bytes.NewBufferString("1234")
- reader := PaddedReader(buf, 3)
+ reader := PaddedReader(buf, 3, true)
var out [3]byte
n, err := io.ReadFull(reader, out[:])
if err != nil {
@@ -84,14 +88,80 @@ func TestManyBlocks(t *testing.T) {
t.Errorf("should have read 3 bytes, read %d", n)
}
+ // PaddedReader constructor parameter "insertPadLengthAll" is true,
+ // means all last bytes equate the padding length
if out[1] != 2 || out[2] != 2 {
t.Errorf("last values were expected to be 2, got [%x %x]", out[1], out[2])
}
}
+func TestOneBlock_Random(t *testing.T) {
+ buf := bytes.NewBufferString("4321")
+ reader := PaddedReader(buf, 6, false)
+ var out [12]byte
+ n, err := reader.Read(out[:])
+ if err != nil && err != io.EOF {
+ t.Error(err)
+ }
+ if n != 6 {
+ t.Errorf("should have read 6 bytes, read %d", n)
+ }
+
+ // the PaddedReader constructor parameter "insertPadLengthAll" is false,
+ // so only the last byte out[2] equates the padding length (the others are random)
+ if out[4] == 2 || out[5] != 2 {
+ t.Errorf("last values were expected to be [random, 2], got [%x %x]", out[4], out[5])
+ }
+}
+
+func TestFullPadding_Random(t *testing.T) {
+ buf := bytes.NewBufferString("1234")
+ reader := PaddedReader(buf, 4, false)
+
+ var out [8]byte
+ n, err := io.ReadFull(reader, out[:])
+ if err != nil {
+ t.Error(err)
+ }
+ if n != 8 {
+ t.Error("should have read 8 bytes, read %d", n)
+ }
+
+ // the PaddedReader constructor parameter "insertPadLengthAll" is false,
+ // so only the last byte out[7] equates the padding length (the others are random)
+ if out[4] == 4 || out[5] == 4 || out[6] == 4 || out[7] != 4 {
+ t.Errorf("last values were expected to be [random, random, random, 4], got [%x %x %x %x]", out[4], out[5], out[6], out[7])
+ }
+}
+
+func TestManyBlocks_Random(t *testing.T) {
+ buf := bytes.NewBufferString("1234")
+ reader := PaddedReader(buf, 3, false)
+ var out [3]byte
+ n, err := io.ReadFull(reader, out[:])
+ if err != nil {
+ t.Error(err)
+ }
+
+ n, err = io.ReadFull(reader, out[:])
+ if err != nil {
+ t.Error(err)
+ }
+
+ if n != 3 {
+ t.Errorf("should have read 3 bytes, read %d", n)
+ }
+
+ // the PaddedReader constructor parameter "insertPadLengthAll" is false,
+ // so only the last byte out[2] equates the padding length (the others are random)
+ if out[1] == 2 || out[2] != 2 {
+ t.Errorf("last values were expected to be [random 2], got [%x %x]", out[1], out[2])
+ }
+}
+
func TestPaddingInMultipleCalls(t *testing.T) {
buf := bytes.NewBufferString("1")
- reader := PaddedReader(buf, 6)
+ reader := PaddedReader(buf, 6, false)
var out [3]byte
n, err := io.ReadFull(reader, out[:])
@@ -122,7 +192,7 @@ func (r failingReader) Read(buf []byte) (int, error) {
}
func TestFailingReader(t *testing.T) {
- reader := PaddedReader(failingReader{}, 8)
+ reader := PaddedReader(failingReader{}, 8, false)
var out [8]byte
_, err := io.ReadFull(reader, out[:])
diff --git a/frontend/api/common.go b/frontend/api/common.go
new file mode 100644
index 00000000..c18e108d
--- /dev/null
+++ b/frontend/api/common.go
@@ -0,0 +1,106 @@
+// Copyright (c) 2016 Readium Foundation
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation and/or
+// other materials provided with the distribution.
+// 3. Neither the name of the organization nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package staticapi
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/readium/readium-lcp-server/api"
+ "github.com/readium/readium-lcp-server/frontend/webpublication"
+ "github.com/readium/readium-lcp-server/frontend/webpurchase"
+ "github.com/readium/readium-lcp-server/frontend/webrepository"
+ "github.com/readium/readium-lcp-server/frontend/webuser"
+)
+
+//IServer defines methods for db interaction
+type IServer interface {
+ RepositoryAPI() webrepository.WebRepository
+ PublicationAPI() webpublication.WebPublication
+ UserAPI() webuser.WebUser
+ PurchaseAPI() webpurchase.WebPurchase
+}
+
+// Pagination used to paginate listing
+type Pagination struct {
+ Page int
+ PerPage int
+}
+
+// ExtractPaginationFromRequest extract from http.Request pagination information
+func ExtractPaginationFromRequest(r *http.Request) (Pagination, error) {
+ var err error
+ var page int64 // default: page 1
+ var perPage int64 // default: 30 items per page
+ pagination := Pagination{}
+
+ if r.FormValue("page") != "" {
+ page, err = strconv.ParseInt((r).FormValue("page"), 10, 32)
+ if err != nil {
+ return pagination, err
+ }
+ } else {
+ page = 1
+ }
+
+ if r.FormValue("per_page") != "" {
+ perPage, err = strconv.ParseInt((r).FormValue("per_page"), 10, 32)
+ if err != nil {
+ return pagination, err
+ }
+ } else {
+ perPage = 30
+ }
+
+ if page > 0 {
+ page-- //pagenum starting at 0 in code, but user interface starting at 1
+ }
+
+ if page < 0 {
+ return pagination, err
+ }
+
+ pagination.Page = int(page)
+ pagination.PerPage = int(perPage)
+ return pagination, err
+}
+
+// PrepareListHeaderResponse Set http headers
+func PrepareListHeaderResponse(
+ resourceCount int,
+ resourceLink string,
+ pagination Pagination,
+ w http.ResponseWriter) {
+ if resourceCount > 0 {
+ nextPage := strconv.Itoa(int(pagination.Page) + 1)
+ w.Header().Set("Link", "<"+resourceLink+"?page="+nextPage+">; rel=\"next\"; title=\"next\"")
+ }
+ if pagination.Page > 1 {
+ previousPage := strconv.Itoa(int(pagination.Page) - 1)
+ w.Header().Set("Link", "<"+resourceLink+"/?page="+previousPage+">; rel=\"previous\"; title=\"previous\"")
+ }
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+}
diff --git a/frontend/api/publication.go b/frontend/api/publication.go
new file mode 100644
index 00000000..b774e972
--- /dev/null
+++ b/frontend/api/publication.go
@@ -0,0 +1,213 @@
+// Copyright (c) 2016 Readium Foundation
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation and/or
+// other materials provided with the distribution.
+// 3. Neither the name of the organization nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package staticapi
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+
+ "github.com/gorilla/mux"
+ "github.com/readium/readium-lcp-server/api"
+ "github.com/readium/readium-lcp-server/frontend/webpublication"
+ "github.com/readium/readium-lcp-server/problem"
+)
+
+//GetPublications returns a list of publications
+func GetPublications(w http.ResponseWriter, r *http.Request, s IServer) {
+ var page int64
+ var perPage int64
+ var err error
+
+ if r.FormValue("page") != "" {
+ page, err = strconv.ParseInt((r).FormValue("page"), 10, 32)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ } else {
+ page = 1
+ }
+
+ if r.FormValue("per_page") != "" {
+ perPage, err = strconv.ParseInt((r).FormValue("per_page"), 10, 32)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ } else {
+ perPage = 30
+ }
+
+ if page > 0 {
+ page-- //pagenum starting at 0 in code, but user interface starting at 1
+ }
+
+ if page < 0 {
+ problem.Error(w, r, problem.Problem{Detail: "page must be positive integer"}, http.StatusBadRequest)
+ return
+ }
+
+ pubs := make([]webpublication.Publication, 0)
+ //log.Println("ListAll(" + strconv.Itoa(int(per_page)) + "," + strconv.Itoa(int(page)) + ")")
+ fn := s.PublicationAPI().List(int(perPage), int(page))
+ for it, err := fn(); err == nil; it, err = fn() {
+ pubs = append(pubs, it)
+ }
+ if len(pubs) > 0 {
+ nextPage := strconv.Itoa(int(page) + 1)
+ w.Header().Set("Link", "; rel=\"next\"; title=\"next\"")
+ }
+ if page > 1 {
+ previousPage := strconv.Itoa(int(page) - 1)
+ w.Header().Set("Link", "; rel=\"previous\"; title=\"previous\"")
+ }
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(pubs)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+}
+
+// GetPublicationByUUID searches a publication by its uuid
+func GetPublication(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ var id int
+ var err error
+ if id, err = strconv.Atoi(vars["id"]); err != nil {
+ // id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "Plublication ID must be an integer"}, http.StatusBadRequest)
+ }
+
+ if pub, err := s.PublicationAPI().Get(int64(id)); err == nil {
+ enc := json.NewEncoder(w)
+ if err = enc.Encode(pub); err == nil {
+ // send json of correctly encoded user info
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ } else {
+ switch err {
+ case webpublication.ErrNotFound:
+ {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ }
+ default:
+ {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ }
+ }
+}
+
+//DecodeJSONUser transforms a json string to a User struct
+func DecodeJSONPublication(r *http.Request) (webpublication.Publication, error) {
+ var dec *json.Decoder
+ if ctype := r.Header["Content-Type"]; len(ctype) > 0 && ctype[0] == api.ContentType_JSON {
+ dec = json.NewDecoder(r.Body)
+ }
+ pub := webpublication.Publication{}
+ err := dec.Decode(&pub)
+ return pub, err
+}
+
+// CreatePublication creates a publication in the database
+func CreatePublication(w http.ResponseWriter, r *http.Request, s IServer) {
+ var pub webpublication.Publication
+ var err error
+ if pub, err = DecodeJSONPublication(r); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: "incorrect JSON Publication " + err.Error()}, http.StatusBadRequest)
+ return
+ }
+ // publication ok
+ if err := s.PublicationAPI().Add(pub); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+
+ // publication added to db
+ w.WriteHeader(http.StatusCreated)
+}
+
+// UpdatePublication updates an identified publication (id) in the database
+func UpdatePublication(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ var id int
+ var err error
+ var pub webpublication.Publication
+ if id, err = strconv.Atoi(vars["id"]); err != nil {
+ // id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "Plublication ID must be an integer"}, http.StatusBadRequest)
+ return
+ }
+ // ID is a number, check publication (json)
+ if pub, err = DecodeJSONPublication(r); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ // publication ok, id is a number, search publication to update
+ if foundPub, err := s.PublicationAPI().Get(int64(id)); err != nil {
+ switch err {
+ case webpublication.ErrNotFound:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ default:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ } else {
+ // publication is found!
+ if err := s.PublicationAPI().Update(webpublication.Publication{
+ ID: foundPub.ID,
+ Title: pub.Title,
+ Status: foundPub.Status}); err != nil {
+ //update failed!
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ //database update ok
+ w.WriteHeader(http.StatusOK)
+ //return
+ }
+}
+
+// DeletePublication removes a publication in the database
+func DeletePublication(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ id, err := strconv.ParseInt(vars["id"], 10, 64)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ if err := s.PublicationAPI().Delete(id); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ // publication deleted from db
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/frontend/api/purchase.go b/frontend/api/purchase.go
new file mode 100644
index 00000000..851f7118
--- /dev/null
+++ b/frontend/api/purchase.go
@@ -0,0 +1,314 @@
+// Copyright (c) 2016 Readium Foundation
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation and/or
+// other materials provided with the distribution.
+// 3. Neither the name of the organization nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package staticapi
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "strconv"
+
+ "github.com/gorilla/mux"
+ "github.com/readium/readium-lcp-server/api"
+ "github.com/readium/readium-lcp-server/frontend/webpurchase"
+ "github.com/readium/readium-lcp-server/license"
+ "github.com/readium/readium-lcp-server/problem"
+
+ "github.com/Machiel/slugify"
+)
+
+//DecodeJSONPurchase transforms a json string to a User struct
+func DecodeJSONPurchase(r *http.Request) (webpurchase.Purchase, error) {
+ var dec *json.Decoder
+ if ctype := r.Header["Content-Type"]; len(ctype) > 0 && ctype[0] == api.ContentType_JSON {
+ dec = json.NewDecoder(r.Body)
+ }
+ purchase := webpurchase.Purchase{}
+ err := dec.Decode(&purchase)
+ return purchase, err
+}
+
+// GetPurchases searches all purchases for a client
+func GetPurchases(w http.ResponseWriter, r *http.Request, s IServer) {
+ var err error
+
+ pagination, err := ExtractPaginationFromRequest(r)
+ if err != nil {
+ // user id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "Pagination error"}, http.StatusBadRequest)
+ return
+ }
+
+ purchases := make([]webpurchase.Purchase, 0)
+ fn := s.PurchaseAPI().List(pagination.PerPage, pagination.Page)
+
+ for it, err := fn(); err == nil; it, err = fn() {
+ purchases = append(purchases, it)
+ }
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(purchases)
+ PrepareListHeaderResponse(len(purchases), "/api/v1/purchases", pagination, w)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+}
+
+//GetUserPurchases searches all purchases for a client
+func GetUserPurchases(w http.ResponseWriter, r *http.Request, s IServer) {
+ var err error
+ var userId int64
+ vars := mux.Vars(r)
+
+ if userId, err = strconv.ParseInt(vars["user_id"], 10, 64); err != nil {
+ // user id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "User ID must be an integer"}, http.StatusBadRequest)
+ return
+ }
+
+ pagination, err := ExtractPaginationFromRequest(r)
+ if err != nil {
+ // user id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "Pagination error"}, http.StatusBadRequest)
+ return
+ }
+
+ purchases := make([]webpurchase.Purchase, 0)
+ fn := s.PurchaseAPI().ListByUser(userId, pagination.PerPage, pagination.Page)
+ for it, err := fn(); err == nil; it, err = fn() {
+ purchases = append(purchases, it)
+ }
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(purchases)
+ PrepareListHeaderResponse(len(purchases), "/api/v1/users/"+vars["user_id"]+"/purchases", pagination, w)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+}
+
+//CreatePurchase creates a purchase in the database
+func CreatePurchase(w http.ResponseWriter, r *http.Request, s IServer) {
+ var purchase webpurchase.Purchase
+ var err error
+ if purchase, err = DecodeJSONPurchase(r); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: "incorrect JSON Purchase " + err.Error()}, http.StatusBadRequest)
+ return
+ }
+
+ // purchase ok
+ if err = s.PurchaseAPI().Add(purchase); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ return
+ }
+
+ // publication added to db
+ w.WriteHeader(http.StatusCreated)
+}
+
+//GetPurchaseLicenseFromLicenseUUID() finds the purchase ID from a given license UUID (passed in URL),
+//and performs the same as GetPurchaseLicense(), returning "license.lcpl" filename
+//(as this API is meant to be accessed from the LSD JSON license link)
+func GetPurchaseLicenseFromLicenseUUID(w http.ResponseWriter, r *http.Request, s IServer) {
+
+ vars := mux.Vars(r)
+ var purchase webpurchase.Purchase
+ var err error
+
+ if purchase, err = s.PurchaseAPI().GetByLicenseID(vars["licenseID"]); err != nil {
+ switch err {
+ case webpurchase.ErrNotFound:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ default:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ return
+ }
+
+ fullLicense, err := s.PurchaseAPI().GenerateLicense(purchase)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ return
+ }
+
+ //attachmentName := slugify.Slugify(purchase.Publication.Title)
+ w.Header().Set("Content-Type", api.ContentType_LCP_JSON)
+ w.Header().Set("Content-Disposition", "attachment; filename=\"license.lcpl\"")
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(fullLicense)
+
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ return
+ }
+}
+
+//GetPurchaseLicense contacts LCP server and asks a license for the purchase using the partial license and resourceID
+func GetPurchaseLicense(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ var id int
+ var err error
+
+ if id, err = strconv.Atoi(vars["id"]); err != nil {
+ // id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "Purchase ID must be an integer"}, http.StatusBadRequest)
+ return
+ }
+
+ purchase, err := s.PurchaseAPI().Get(int64(id))
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ return
+ }
+
+ fullLicense, err := s.PurchaseAPI().GenerateLicense(purchase)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ return
+ }
+
+ attachmentName := slugify.Slugify(purchase.Publication.Title)
+ w.Header().Set("Content-Type", api.ContentType_LCP_JSON)
+ w.Header().Set("Content-Disposition", "attachment; filename=\""+attachmentName+".lcpl\"")
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(fullLicense)
+
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ return
+ }
+}
+
+//GetPurchase gets a purchase by its ID in the database
+func GetPurchase(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ var id int
+ var err error
+ if id, err = strconv.Atoi(vars["id"]); err != nil {
+ // id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "Purchase ID must be an integer"}, http.StatusBadRequest)
+ return
+ }
+
+ purchase, err := s.PurchaseAPI().Get(int64(id))
+ if err != nil {
+ switch err {
+ case webpurchase.ErrNotFound:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ default:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ return
+ }
+
+ // purchase found
+ // purchase.PartialLicense = "*" //hide partialLicense?
+ enc := json.NewEncoder(w)
+ if err = enc.Encode(purchase); err == nil {
+ // send json of correctly encoded user info
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+}
+
+//GetPurchaseByLicenseID gets a purchase by a LicenseID in the database
+func GetPurchaseByLicenseID(w http.ResponseWriter, r *http.Request, s IServer) {
+ var purchase webpurchase.Purchase
+ vars := mux.Vars(r)
+ var err error
+
+ if purchase, err = s.PurchaseAPI().GetByLicenseID(vars["licenseID"]); err != nil {
+ switch err {
+ case webpurchase.ErrNotFound:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ default:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ return
+ }
+ // purchase found
+ enc := json.NewEncoder(w)
+ if err = enc.Encode(purchase); err == nil {
+ // send json of correctly encoded user info
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+}
+
+// getLicenseInfo decoldes a license in data (bytes, response.body)
+func getLicenseInfo(data []byte, lic *license.License) error {
+ var dec *json.Decoder
+ dec = json.NewDecoder(bytes.NewReader(data))
+ if err := dec.Decode(&lic); err != nil {
+ return err
+ }
+ return nil
+}
+
+//UpdatePurchase updates a purchase in the database
+func UpdatePurchase(w http.ResponseWriter, r *http.Request, s IServer) {
+ var newPurchase webpurchase.Purchase
+ vars := mux.Vars(r)
+ var id int
+ var err error
+ if id, err = strconv.Atoi(vars["id"]); err != nil {
+ // id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "Purchase ID must be an integer"}, http.StatusBadRequest)
+ return
+ }
+ //ID is a number, check user (json)
+ if newPurchase, err = DecodeJSONPurchase(r); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+
+ // purchase found
+ if err := s.PurchaseAPI().Update(webpurchase.Purchase{
+ ID: int64(id),
+ LicenseUUID: newPurchase.LicenseUUID,
+ StartDate: newPurchase.StartDate,
+ EndDate: newPurchase.EndDate,
+ Status: newPurchase.Status}); err != nil {
+
+ switch err {
+ case webpurchase.ErrNotFound:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ default:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/frontend/api/repository.go b/frontend/api/repository.go
new file mode 100644
index 00000000..2eaa2dc5
--- /dev/null
+++ b/frontend/api/repository.go
@@ -0,0 +1,57 @@
+// Copyright (c) 2016 Readium Foundation
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation and/or
+// other materials provided with the distribution.
+// 3. Neither the name of the organization nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package staticapi
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/readium/readium-lcp-server/api"
+ "github.com/readium/readium-lcp-server/frontend/webrepository"
+ "github.com/readium/readium-lcp-server/problem"
+)
+
+// GetRepositoryMasterFiles returns a list of repository masterfiles
+func GetRepositoryMasterFiles(w http.ResponseWriter, r *http.Request, s IServer) {
+ var err error
+
+ files := make([]webrepository.RepositoryFile, 0)
+
+ fn := s.RepositoryAPI().GetMasterFiles()
+
+ for it, err := fn(); err == nil; it, err = fn() {
+ files = append(files, it)
+ }
+
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(files)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+}
diff --git a/frontend/api/user.go b/frontend/api/user.go
new file mode 100644
index 00000000..15c0edee
--- /dev/null
+++ b/frontend/api/user.go
@@ -0,0 +1,205 @@
+// Copyright (c) 2016 Readium Foundation
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation and/or
+// other materials provided with the distribution.
+// 3. Neither the name of the organization nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package staticapi
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+
+ "github.com/gorilla/mux"
+ "github.com/readium/readium-lcp-server/api"
+ "github.com/readium/readium-lcp-server/frontend/webuser"
+ "github.com/readium/readium-lcp-server/problem"
+)
+
+//GetUsers returns a list of users
+func GetUsers(w http.ResponseWriter, r *http.Request, s IServer) {
+ var page int64
+ var perPage int64
+ var err error
+ if r.FormValue("page") != "" {
+ page, err = strconv.ParseInt((r).FormValue("page"), 10, 32)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ } else {
+ page = 1
+ }
+ if r.FormValue("per_page") != "" {
+ perPage, err = strconv.ParseInt((r).FormValue("per_page"), 10, 32)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ } else {
+ perPage = 30
+ }
+ if page > 0 {
+ page-- //pagenum starting at 0 in code, but user interface starting at 1
+ }
+ if page < 0 {
+ problem.Error(w, r, problem.Problem{Detail: "page must be positive integer"}, http.StatusBadRequest)
+ return
+ }
+ users := make([]webuser.User, 0)
+ //log.Println("ListAll(" + strconv.Itoa(int(per_page)) + "," + strconv.Itoa(int(page)) + ")")
+ fn := s.UserAPI().ListUsers(int(perPage), int(page))
+ for it, err := fn(); err == nil; it, err = fn() {
+ users = append(users, it)
+ }
+ if len(users) > 0 {
+ nextPage := strconv.Itoa(int(page) + 1)
+ w.Header().Set("Link", "; rel=\"next\"; title=\"next\"")
+ }
+ if page > 1 {
+ previousPage := strconv.Itoa(int(page) - 1)
+ w.Header().Set("Link", "; rel=\"previous\"; title=\"previous\"")
+ }
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+
+ enc := json.NewEncoder(w)
+ err = enc.Encode(users)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+}
+
+//GetUserByEmail searches a client by his email
+func GetUser(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ id, err := strconv.Atoi(vars["id"])
+ if err != nil {
+ // id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "User ID must be an integer"}, http.StatusBadRequest)
+ }
+ if user, err := s.UserAPI().Get(int64(id)); err == nil {
+ enc := json.NewEncoder(w)
+ if err = enc.Encode(user); err == nil {
+ // send json of correctly encoded user info
+ w.Header().Set("Content-Type", api.ContentType_JSON)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ } else {
+ switch err {
+ case webuser.ErrNotFound:
+ {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ }
+ default:
+ {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ }
+ }
+ return
+}
+
+//DecodeJSONUser transforms a json string to a User struct
+func DecodeJSONUser(r *http.Request) (webuser.User, error) {
+ var dec *json.Decoder
+ if ctype := r.Header["Content-Type"]; len(ctype) > 0 && ctype[0] == api.ContentType_JSON {
+ dec = json.NewDecoder(r.Body)
+ }
+ user := webuser.User{}
+ err := dec.Decode(&user)
+ return user, err
+}
+
+//CreateUser creates a user in the database
+func CreateUser(w http.ResponseWriter, r *http.Request, s IServer) {
+ var user webuser.User
+ var err error
+ if user, err = DecodeJSONUser(r); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: "incorrect JSON User " + err.Error()}, http.StatusBadRequest)
+ return
+ }
+ //user ok
+ if err := s.UserAPI().Add(user); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ // user added to db
+ w.WriteHeader(http.StatusCreated)
+}
+
+//UpdateUser updates an identified user (id) in the database
+func UpdateUser(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ var id int
+ var err error
+ var user webuser.User
+ if id, err = strconv.Atoi(vars["id"]); err != nil {
+ // id is not a number
+ problem.Error(w, r, problem.Problem{Detail: "User ID must be an integer"}, http.StatusBadRequest)
+ return
+ }
+ //ID is a number, check user (json)
+ if user, err = DecodeJSONUser(r); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ // user ok, id is a number, search user to update
+ if _, err := s.UserAPI().Get(int64(id)); err != nil {
+ switch err {
+ case webuser.ErrNotFound:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusNotFound)
+ default:
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ }
+ } else {
+ // client is found!
+ if err := s.UserAPI().Update(webuser.User{ID: int64(id), Name: user.Name, Email: user.Email, Password: user.Password}); err != nil {
+ //update failed!
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError)
+ return
+ }
+ //database update ok
+ w.WriteHeader(http.StatusOK)
+ //return
+ }
+
+}
+
+//DeleteUser creates a user in the database
+func DeleteUser(w http.ResponseWriter, r *http.Request, s IServer) {
+ vars := mux.Vars(r)
+ uid, err := strconv.ParseInt(vars["id"], 10, 64)
+ if err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ if err := s.UserAPI().DeleteUser(uid); err != nil {
+ problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest)
+ return
+ }
+ // user added to db
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/frontend/frontend.go b/frontend/frontend.go
new file mode 100644
index 00000000..d44869b1
--- /dev/null
+++ b/frontend/frontend.go
@@ -0,0 +1,169 @@
+// Copyright (c) 2016 Readium Foundation
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation and/or
+// other materials provided with the distribution.
+// 3. Neither the name of the organization nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package main
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "syscall"
+
+ _ "github.com/go-sql-driver/mysql"
+ _ "github.com/lib/pq"
+ _ "github.com/mattn/go-sqlite3"
+
+ "github.com/readium/readium-lcp-server/config"
+ "github.com/readium/readium-lcp-server/frontend/server"
+ "github.com/readium/readium-lcp-server/frontend/webpublication"
+ "github.com/readium/readium-lcp-server/frontend/webpurchase"
+ "github.com/readium/readium-lcp-server/frontend/webrepository"
+ "github.com/readium/readium-lcp-server/frontend/webuser"
+)
+
+func dbFromURI(uri string) (string, string) {
+ parts := strings.Split(uri, "://")
+ return parts[0], parts[1]
+}
+
+func main() {
+ var dbURI, static, configFile string
+ var err error
+
+ if configFile = os.Getenv("READIUM_WEBTEST_CONFIG"); configFile == "" {
+ configFile = "config.yaml"
+ }
+ config.ReadConfig(configFile)
+ log.Println("Read config from " + configFile)
+
+ err = config.SetPublicUrls()
+ if err != nil {
+ panic(err)
+ }
+
+ log.Println("LCP server = " + config.Config.LcpServer.PublicBaseUrl)
+ log.Println("using login " + config.Config.LcpUpdateAuth.Username)
+
+ if dbURI = config.Config.FrontendServer.Database; dbURI == "" {
+ dbURI = "sqlite3://file:frontend.sqlite?cache=shared&mode=rwc"
+ }
+ driver, cnxn := dbFromURI(dbURI)
+ db, err := sql.Open(driver, cnxn)
+ if err != nil {
+ panic(err)
+ }
+ _, err = db.Exec("PRAGMA journal_mode = WAL")
+ if err != nil {
+ panic(err)
+ }
+
+ repoManager, err := webrepository.Init(config.Config.FrontendServer)
+ if err != nil {
+ panic(err)
+ }
+
+ publicationDB, err := webpublication.Init(config.Config, db)
+ if err != nil {
+ panic(err)
+ }
+
+ userDB, err := webuser.Open(db)
+ if err != nil {
+ panic(err)
+ }
+
+ purchaseDB, err := webpurchase.Init(config.Config, db)
+ if err != nil {
+ panic(err)
+ }
+
+ static = config.Config.FrontendServer.Directory
+ if static == "" {
+ _, file, _, _ := runtime.Caller(0)
+ here := filepath.Dir(file)
+ static = filepath.Join(here, "../frontend/manage")
+ }
+
+ filepathConfigJs := filepath.Join(static, "config.js")
+ fileConfigJs, err := os.Create(filepathConfigJs)
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ if err := fileConfigJs.Close(); err != nil {
+ panic(err)
+ }
+ }()
+
+ configJs := `
+ // This file is automatically generated, and git-ignored.
+ // To ignore your local changes, use:
+ // git update-index --assume-unchanged frontend/manage/config.js
+ window.Config = {`
+ configJs += "\n\tfrontend: {url: '" + config.Config.FrontendServer.PublicBaseUrl + "' },\n"
+ configJs += "\tlcp: {url: '" + config.Config.LcpServer.PublicBaseUrl + "'},\n"
+ configJs += "\tlsd: {url: '" + config.Config.LsdServer.PublicBaseUrl + "'}\n}"
+
+ log.Println("manage/index.html config.js:")
+ log.Println(configJs)
+
+ fileConfigJs.WriteString(configJs)
+ HandleSignals()
+ s := frontend.New(config.Config.FrontendServer.Host+":"+strconv.Itoa(config.Config.FrontendServer.Port), static, repoManager, publicationDB, userDB, purchaseDB)
+ log.Println("Frontend webserver for LCP running on " + config.Config.FrontendServer.Host + ":" + strconv.Itoa(config.Config.FrontendServer.Port))
+ log.Println("using database " + dbURI)
+
+ if err := s.ListenAndServe(); err != nil {
+ log.Println("Error " + err.Error())
+ }
+}
+
+// HandleSignals handles system signals and adds a log before quitting
+func HandleSignals() {
+ sigChan := make(chan os.Signal)
+ go func() {
+ stacktrace := make([]byte, 1<<20)
+ for sig := range sigChan {
+ switch sig {
+ case syscall.SIGQUIT:
+ length := runtime.Stack(stacktrace, true)
+ fmt.Println(string(stacktrace[:length]))
+ case syscall.SIGINT:
+ fallthrough
+ case syscall.SIGTERM:
+ fmt.Println("Shutting down...")
+ os.Exit(0)
+ }
+ }
+ }()
+ signal.Notify(sigChan, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM)
+}
diff --git a/frontend/manage/.editorconfig b/frontend/manage/.editorconfig
new file mode 100644
index 00000000..928602c5
--- /dev/null
+++ b/frontend/manage/.editorconfig
@@ -0,0 +1,28 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# 4 spaces indentation
+[*.{ts,scss,css}]
+indent_style = space
+indent_size = 4
+
+# 2 spaces indentation
+[*.{html}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+max_line_length = 0
+trim_trailing_whitespace = false
+
+# Indentation override
+#[lib/**.js]
+#[{package.json,.travis.yml}]
+#[**/**.js]
diff --git a/frontend/manage/.travis.yml b/frontend/manage/.travis.yml
new file mode 100644
index 00000000..20ff41e7
--- /dev/null
+++ b/frontend/manage/.travis.yml
@@ -0,0 +1,20 @@
+dist: trusty
+sudo: required
+language: node_js
+node_js:
+ - "5"
+os:
+ - linux
+env:
+ global:
+ - DBUS_SESSION_BUS_ADDRESS=/dev/null
+ - DISPLAY=:99.0
+ - CHROME_BIN=chromium-browser
+before_script:
+ - sh -e /etc/init.d/xvfb start
+install:
+ - npm install
+script:
+ - npm run lint
+ - npm run test-once
+ - npm run e2e
diff --git a/frontend/manage/LICENSE-angular-quickstart b/frontend/manage/LICENSE-angular-quickstart
new file mode 100644
index 00000000..51b127e8
--- /dev/null
+++ b/frontend/manage/LICENSE-angular-quickstart
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2014-2016 Google, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/frontend/manage/README.md b/frontend/manage/README.md
new file mode 100644
index 00000000..4b8baf4d
--- /dev/null
+++ b/frontend/manage/README.md
@@ -0,0 +1,101 @@
+# see Angular QuickStart Source (github)
+
+## Prerequisites
+
+Node.js and npm are essential to Angular development.
+
+
+Get it now if it's not already installed on your machine.
+
+**Verify that you are running at least node `v4.x.x` and npm `3.x.x`**
+by running `node -v` and `npm -v` in a terminal/console window.
+Older versions produce errors.
+
+We recommend [nvm](https://github.com/creationix/nvm) for managing multiple versions of node and npm.
+
+## Install npm packages
+
+> See npm and nvm version notes above
+
+Install the npm packages described in the `package.json` and verify that it works:
+
+```bash
+npm install
+npm start
+```
+
+Only for development: (This angular project should be served by staticserver (go project))
+
+The `npm start` command first compiles the application,
+then simultaneously re-compiles and runs the `lite-server`.
+Both the compiler and the server watch for file changes.
+
+Shut it down manually with `Ctrl-C`.
+Index.html contains angular test lcp project
+The old index.html is now renamed to manage.html
+
+### npm scripts
+
+We've captured many of the most useful commands in npm scripts defined in the `package.json`:
+
+* `npm start` - runs the compiler and a server at the same time, both in "watch mode".
+* `npm run tsc` - runs the TypeScript compiler once.
+* `npm run tsc:w` - runs the TypeScript compiler in watch mode; the process keeps running, awaiting changes to TypeScript files and re-compiling when it sees them.
+* `npm run lite` - runs the [lite-server](https://www.npmjs.com/package/lite-server), a light-weight, static file server, written and maintained by
+[John Papa](https://github.com/johnpapa) and
+[Christopher Martin](https://github.com/cgmartin)
+with excellent support for Angular apps that use routing.
+
+Here are the test related scripts:
+* `npm test` - compiles, runs and watches the karma unit tests
+* `npm run e2e` - run protractor e2e tests, written in JavaScript (*e2e-spec.js)
+
+## Testing
+
+karma/jasmine unit test and protractor end-to-end testing support.
+
+These tools are configured for specific conventions described below.
+
+*It is unwise and rarely possible to run the application, the unit tests, and the e2e tests at the same time.
+We recommend that you shut down one before starting another.*
+
+### Unit Tests
+TypeScript unit-tests are usually in the `app` folder. Their filenames must end in `.spec`.
+
+Look for the example `app/app.component.spec.ts`.
+Add more `.spec.ts` files as you wish; we configured karma to find them.
+
+Run it with `npm test`
+
+That command first compiles the application, then simultaneously re-compiles and runs the karma test-runner.
+Both the compiler and the karma watch for (different) file changes.
+
+Shut it down manually with `Ctrl-C`.
+
+Test-runner output appears in the terminal window.
+We can update our app and our tests in real-time, keeping a weather eye on the console for broken tests.
+Karma is occasionally confused and it is often necessary to shut down its browser or even shut the command down (`Ctrl-C`) and
+restart it. No worries; it's pretty quick.
+
+### End-to-end (E2E) Tests
+
+E2E tests are in the `e2e` directory, side by side with the `app` folder.
+Their filenames must end in `.e2e-spec.ts`.
+
+Look for the example `e2e/app.e2e-spec.ts`.
+Add more `.e2e-spec.js` files as you wish (although one usually suffices for small projects);
+we configured protractor to find them.
+
+Thereafter, run them with `npm run e2e`.
+
+That command first compiles, then simultaneously starts the Http-Server at `localhost:8080`
+and launches protractor.
+
+The pass/fail test results appear at the bottom of the terminal window.
+A custom reporter (see `protractor.config.js`) generates a `./_test-output/protractor-results.txt` file
+which is easier to read; this file is excluded from source control.
+
+Shut it down manually with `Ctrl-C`.
+
+[travis-badge]: https://travis-ci.org/angular/quickstart.svg?branch=master
+[travis-badge-url]: https://travis-ci.org/angular/quickstart
diff --git a/frontend/manage/app/app-routing.module.ts b/frontend/manage/app/app-routing.module.ts
new file mode 100644
index 00000000..d2ab1e3f
--- /dev/null
+++ b/frontend/manage/app/app-routing.module.ts
@@ -0,0 +1,19 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { PageNotFoundComponent } from './not-found.component';
+
+const appRoutes: Routes = [
+ { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
+ { path: '**', component: PageNotFoundComponent }
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forRoot(appRoutes)
+ ],
+ exports: [
+ RouterModule
+ ]
+})
+
+export class AppRoutingModule {}
diff --git a/frontend/manage/app/app.component.html b/frontend/manage/app/app.component.html
new file mode 100644
index 00000000..5b7fbdf9
--- /dev/null
+++ b/frontend/manage/app/app.component.html
@@ -0,0 +1,13 @@
+