├── .gitignore ├── LICENSE ├── server ├── Makefile ├── adapters │ ├── blockchains │ │ ├── errors.go │ │ ├── ethereum.go │ │ ├── mock.go │ │ └── mock_test.go │ ├── crypto │ │ ├── crypto.go │ │ └── crypto_test.go │ ├── repositories │ │ ├── mock.go │ │ └── mock_test.go │ ├── servers │ │ ├── download.go │ │ ├── http.go │ │ ├── log.go │ │ └── upload.go │ └── storages │ │ ├── mock.go │ │ └── mock_test.go ├── config │ └── config.go ├── db │ └── schema.sql ├── domain │ ├── entities │ │ └── file.go │ └── repositories │ │ ├── errors.go │ │ └── file.go ├── log │ └── log.go ├── main.go ├── presenters │ ├── http.go │ └── http_test.go ├── usecases │ ├── dto │ │ └── transaction.go │ ├── errors.go │ ├── interactor.go │ ├── ports │ │ ├── blockchain.go │ │ ├── crypto.go │ │ ├── server.go │ │ └── storage.go │ ├── usecases.go │ └── usecases_test.go └── utils │ └── utils.go └── web ├── .babelrc ├── contracts ├── Cris.sol └── Migrations.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── public ├── css │ ├── main.css │ └── riot-mui.css ├── index.html └── js │ ├── riot-mui.js │ └── web3.min.js ├── scripts └── setup.sh ├── src ├── js │ ├── actions │ │ ├── status.js │ │ └── upload.js │ ├── clients │ │ ├── api.js │ │ └── ethereum.js │ ├── index.js │ ├── share.js │ └── stores │ │ └── store.js └── tags │ ├── app.tag │ ├── status.tag │ └── upload.tag ├── truffle.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/8edb8a95c4c4b3dce71a378aaaf89275510b9cef/go.gitignore 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | 18 | 19 | cris 20 | ### https://raw.github.com/github/gitignore/cc542de017c606138a87ee4880e5f06b3a306def/node.gitignore 21 | 22 | # Logs 23 | logs 24 | *.log 25 | npm-debug.log* 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | 38 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules 49 | jspm_packages 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | web/public/js/app.js 58 | web/build 59 | web/.truffle-solidity-loader 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 lycoris0731 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: test 4 | test: 5 | go test -v -race ./... 6 | 7 | -------------------------------------------------------------------------------- /server/adapters/blockchains/errors.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import "errors" 4 | 5 | type errTxNotFound struct { 6 | error 7 | } 8 | 9 | func (e errTxNotFound) ClientError() bool { 10 | return true 11 | } 12 | 13 | var ( 14 | ErrTxNotFound = errTxNotFound{errors.New("no such transaction")} 15 | ) 16 | -------------------------------------------------------------------------------- /server/adapters/blockchains/ethereum.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import "github.com/ktr0731/cris/server/usecases/dto" 4 | 5 | type EthereumAdapter struct{} 6 | 7 | func (a *EthereumAdapter) FindTxByHash(hash string) (*dto.Transaction, error) { 8 | return nil, nil 9 | } 10 | -------------------------------------------------------------------------------- /server/adapters/blockchains/mock.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | 8 | "github.com/ktr0731/cris/server/usecases/dto" 9 | "golang.org/x/sync/syncmap" 10 | ) 11 | 12 | type MockBlockchainAdapter struct { 13 | store syncmap.Map 14 | } 15 | 16 | func (a *MockBlockchainAdapter) FindTxByHash(hash string) (*dto.Transaction, error) { 17 | v, ok := a.store.Load(hash) 18 | if !ok { 19 | return nil, ErrTxNotFound 20 | } 21 | tx, ok := v.(*dto.Transaction) 22 | if !ok { 23 | return nil, errors.New("type assertion failed") 24 | } 25 | return tx, nil 26 | } 27 | 28 | // CreateTx is used to testing only 29 | func (a *MockBlockchainAdapter) CreateTx(hash string, content io.Reader) (*dto.Transaction, error) { 30 | b, err := ioutil.ReadAll(content) 31 | if err != nil { 32 | return nil, err 33 | } 34 | tx := &dto.Transaction{ 35 | HashedData: string(b), 36 | } 37 | a.store.Store(hash, tx) 38 | return tx, nil 39 | } 40 | 41 | func NewMockBlockchain() *MockBlockchainAdapter { 42 | return &MockBlockchainAdapter{} 43 | } 44 | -------------------------------------------------------------------------------- /server/adapters/blockchains/mock_test.go: -------------------------------------------------------------------------------- 1 | package blockchains 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMockBlockchainAdapter_FindTxByHash(t *testing.T) { 12 | adapter := NewMockBlockchain() 13 | hash := "C204" 14 | content := strings.NewReader("Cristina") 15 | expected, err := adapter.CreateTx(hash, content) 16 | require.NoError(t, err) 17 | actual, err := adapter.FindTxByHash(hash) 18 | require.NoError(t, err) 19 | 20 | assert.Exactly(t, expected, actual) 21 | } 22 | -------------------------------------------------------------------------------- /server/adapters/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/sha256" 5 | 6 | "golang.org/x/crypto/ed25519" 7 | ) 8 | 9 | type CryptoAdapter struct{} 10 | 11 | func (c *CryptoAdapter) HashDigest(src []byte) string { 12 | sum := sha256.Sum256(src) 13 | return string(sum[:]) 14 | } 15 | 16 | func (c *CryptoAdapter) Verify(pubkey, msg, signature []byte) bool { 17 | return ed25519.Verify(ed25519.PublicKey(pubkey), msg, signature) 18 | } 19 | 20 | func NewCryptoAdapter() *CryptoAdapter { 21 | return &CryptoAdapter{} 22 | } 23 | -------------------------------------------------------------------------------- /server/adapters/crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/sha256" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCryptoAdapter_HashDigest(t *testing.T) { 11 | content := []byte("future gadget") 12 | sum := sha256.Sum256(content) 13 | expected := string(sum[:]) 14 | 15 | assert.Equal(t, expected, NewCryptoAdapter().HashDigest(content)) 16 | } 17 | -------------------------------------------------------------------------------- /server/adapters/repositories/mock.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "github.com/ktr0731/cris/server/config" 5 | "github.com/ktr0731/cris/server/domain/entities" 6 | "github.com/ktr0731/cris/server/domain/repositories" 7 | "github.com/ktr0731/cris/server/log" 8 | "golang.org/x/sync/syncmap" 9 | ) 10 | 11 | type MockFileRepositoryAdapter struct { 12 | logger *log.Logger 13 | config *config.Config 14 | 15 | storage syncmap.Map 16 | } 17 | 18 | func NewMockFileRepository(logger *log.Logger, config *config.Config) *MockFileRepositoryAdapter { 19 | return &MockFileRepositoryAdapter{ 20 | logger: logger, 21 | config: config, 22 | } 23 | } 24 | 25 | func (r *MockFileRepositoryAdapter) Store(e *entities.File) (entities.FileID, error) { 26 | r.storage.Store(e.ID, e) 27 | r.logger.Printf("[MockFileRepo] stored an entity: %s", e.ID) 28 | return e.ID, nil 29 | } 30 | 31 | func (r *MockFileRepositoryAdapter) Find(id entities.FileID) (*entities.File, error) { 32 | v, ok := r.storage.Load(id) 33 | if !ok { 34 | return nil, repositories.ErrNotFound 35 | } 36 | e, ok := v.(*entities.File) 37 | if !ok { 38 | return nil, repositories.ErrNotFound 39 | } 40 | return e, nil 41 | } 42 | 43 | func (r *MockFileRepositoryAdapter) Remove(id entities.FileID) (*entities.File, error) { 44 | e, err := r.Find(id) 45 | if err != nil { 46 | return nil, err 47 | } 48 | r.storage.Delete(id) 49 | r.logger.Printf("[MockFileRepo] remove an entity: %s", e.ID) 50 | return e, nil 51 | } 52 | -------------------------------------------------------------------------------- /server/adapters/repositories/mock_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ktr0731/cris/server/domain/entities" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMockFileRepositoryAdapter_Store(t *testing.T) { 12 | repository := NewMockFileRepository(nil, nil) 13 | expected := entities.NewFile("url") 14 | _, err := repository.Store(expected) 15 | require.NoError(t, err) 16 | v, ok := repository.storage.Load(expected.ID) 17 | require.True(t, ok) 18 | actual, ok := v.(*entities.File) 19 | require.True(t, ok) 20 | assert.Equal(t, expected, actual) 21 | } 22 | 23 | func TestMockFileRepositoryAdapter_Find(t *testing.T) { 24 | repository := NewMockFileRepository(nil, nil) 25 | expected := entities.NewFile("url") 26 | _, err := repository.Store(expected) 27 | require.NoError(t, err) 28 | actual, err := repository.Find(expected.ID) 29 | require.NoError(t, err) 30 | assert.Equal(t, expected, actual) 31 | } 32 | 33 | func TestMockFileRepositoryAdapter_Remove(t *testing.T) { 34 | repository := NewMockFileRepository(nil, nil) 35 | expected := entities.NewFile("url") 36 | _, err := repository.Store(expected) 37 | require.NoError(t, err) 38 | _, err = repository.Remove(expected.ID) 39 | require.NoError(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /server/adapters/servers/download.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/ktr0731/cris/server/config" 9 | "github.com/ktr0731/cris/server/domain/entities" 10 | "github.com/ktr0731/cris/server/log" 11 | "github.com/ktr0731/cris/server/usecases/ports" 12 | ) 13 | 14 | type DownloadFileHandler struct { 15 | BaseFileHandler 16 | } 17 | 18 | func newDownloadFileHandler(config *config.Config, inputPort ports.ServerInputPort) http.Handler { 19 | logger, err := log.NewLogger(config) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return withLogging(config, logger, &DownloadFileHandler{ 24 | newBaseFileHandler(logger, inputPort), 25 | }) 26 | } 27 | 28 | func (h *DownloadFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | if r.Method != http.MethodGet { 30 | w.WriteHeader(http.StatusMethodNotAllowed) 31 | return 32 | } 33 | 34 | p := r.URL.Path 35 | 36 | // allowed format: /v1/files/token.hash 37 | sp := strings.Split(p, "/") 38 | if len(sp) != 4 { 39 | w.WriteHeader(http.StatusBadRequest) 40 | return 41 | } 42 | 43 | res, err := h.inputPort.DownloadFile(&ports.DownloadFileParams{ 44 | Token: entities.FileID(sp[3]), 45 | }) 46 | 47 | if err != nil { 48 | h.logger.Printf("[ERR] %s", err) 49 | handleError(w, err) 50 | return 51 | } 52 | 53 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(res.Content))) 54 | if _, err := w.Write(res.Content); err != nil { 55 | w.WriteHeader(http.StatusInternalServerError) 56 | fmt.Fprintln(w, err) 57 | return 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/adapters/servers/http.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/log" 9 | "github.com/ktr0731/cris/server/usecases" 10 | "github.com/ktr0731/cris/server/usecases/ports" 11 | "github.com/rs/cors" 12 | ) 13 | 14 | type Server struct { 15 | logger *log.Logger 16 | config *config.Config 17 | 18 | mux *http.ServeMux 19 | } 20 | 21 | func NewHTTPServer(logger *log.Logger, config *config.Config, inputPort ports.ServerInputPort) *Server { 22 | s := Server{ 23 | logger: logger, 24 | config: config, 25 | mux: http.NewServeMux(), 26 | } 27 | s.mux.Handle(s.getPrefix()+"/files", newUploadFileHandler(config, inputPort)) 28 | s.mux.Handle(s.getPrefix()+"/files/", newDownloadFileHandler(config, inputPort)) 29 | return &s 30 | } 31 | 32 | func (s *Server) Listen() error { 33 | s.logger.Printf("Server listen in %s%s", s.getPrefix(), s.getAddr()) 34 | return http.ListenAndServe(s.getAddr(), cors.Default().Handler(s.mux)) 35 | } 36 | 37 | func (s *Server) getPrefix() string { 38 | return fmt.Sprintf("/%s", s.config.Meta.Version) 39 | } 40 | 41 | func (s *Server) getAddr() string { 42 | return fmt.Sprintf("%s:%s", s.config.Server.Host, s.config.Server.Port) 43 | } 44 | 45 | type BaseFileHandler struct { 46 | logger *log.Logger 47 | 48 | inputPort ports.ServerInputPort 49 | } 50 | 51 | func newBaseFileHandler(logger *log.Logger, inputPort ports.ServerInputPort) BaseFileHandler { 52 | return BaseFileHandler{ 53 | logger: logger, 54 | inputPort: inputPort, 55 | } 56 | } 57 | 58 | func handleError(w http.ResponseWriter, err error) { 59 | if _, ok := err.(usecases.ClientError); ok { 60 | w.WriteHeader(http.StatusBadRequest) 61 | } else { 62 | w.WriteHeader(http.StatusInternalServerError) 63 | } 64 | fmt.Fprintln(w, err) 65 | } 66 | -------------------------------------------------------------------------------- /server/adapters/servers/log.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/ktr0731/cris/server/config" 7 | "github.com/ktr0731/cris/server/log" 8 | ) 9 | 10 | type logHandler struct { 11 | logger *log.Logger 12 | handler http.Handler 13 | } 14 | 15 | func (h *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | h.logger.Printf("%s %s", r.Method, r.URL.Path) 17 | h.handler.ServeHTTP(w, r) 18 | } 19 | 20 | func withLogging(config *config.Config, logger *log.Logger, h http.Handler) http.Handler { 21 | return &logHandler{ 22 | logger: logger, 23 | handler: h, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/adapters/servers/upload.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | 12 | "github.com/ktr0731/cris/server/config" 13 | "github.com/ktr0731/cris/server/log" 14 | "github.com/ktr0731/cris/server/usecases/ports" 15 | ) 16 | 17 | type UploadFileHandler struct { 18 | BaseFileHandler 19 | } 20 | 21 | func newUploadFileHandler(config *config.Config, inputPort ports.ServerInputPort) http.Handler { 22 | logger, err := log.NewLogger(config) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return withLogging(config, logger, &UploadFileHandler{ 27 | newBaseFileHandler(logger, inputPort), 28 | }) 29 | } 30 | 31 | func (h *UploadFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 32 | defer r.Body.Close() 33 | 34 | if r.Method != http.MethodPost { 35 | w.WriteHeader(http.StatusMethodNotAllowed) 36 | return 37 | } 38 | 39 | params := struct { 40 | Content string `json:"content"` 41 | Signature string `json:"signature"` 42 | Pubkey string `json:"pubkey"` 43 | }{} 44 | 45 | // 10 MB 46 | reader := io.LimitReader(r.Body, 10000) 47 | 48 | if err := json.NewDecoder(reader).Decode(¶ms); err != nil { 49 | h.logger.Printf("[ERR] %s", err) 50 | handleError(w, err) 51 | return 52 | } 53 | 54 | content, pubkey, signature := make([]byte, base64.StdEncoding.DecodedLen(len(params.Content))), make([]byte, base64.StdEncoding.DecodedLen(len(params.Pubkey))), make([]byte, base64.StdEncoding.DecodedLen(len(params.Signature))) 55 | var err error 56 | var n int 57 | n, err = base64.StdEncoding.Decode(content, []byte(params.Content)) 58 | if err != nil { 59 | h.logger.Printf("[ERR] %s", err) 60 | handleError(w, err) 61 | return 62 | } 63 | content = content[:n] 64 | 65 | n, err = base64.StdEncoding.Decode(pubkey, []byte(params.Pubkey)) 66 | if err != nil { 67 | h.logger.Printf("[ERR] %s", err) 68 | handleError(w, err) 69 | return 70 | } 71 | pubkey = pubkey[:n] 72 | 73 | n, err = base64.StdEncoding.Decode(signature, []byte(params.Signature)) 74 | if err != nil { 75 | h.logger.Printf("[ERR] %s", err) 76 | handleError(w, err) 77 | return 78 | } 79 | signature = signature[:n] 80 | 81 | fmt.Printf("%x, %x", pubkey, sha256.Sum256(content)) 82 | 83 | res, err := h.inputPort.UploadFile(&ports.UploadFileParams{ 84 | Content: bytes.NewBuffer(content), 85 | Signature: signature, 86 | PubKey: pubkey, 87 | }) 88 | 89 | if err != nil { 90 | h.logger.Printf("[ERR] %s", err) 91 | handleError(w, err) 92 | return 93 | } 94 | 95 | if err := json.NewEncoder(w).Encode(res); err != nil { 96 | w.WriteHeader(http.StatusInternalServerError) 97 | fmt.Fprintln(w, err) 98 | return 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /server/adapters/storages/mock.go: -------------------------------------------------------------------------------- 1 | package storages 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | 10 | "github.com/ktr0731/cris/server/domain/repositories" 11 | "github.com/ktr0731/cris/server/usecases" 12 | "github.com/ktr0731/cris/server/utils" 13 | 14 | "golang.org/x/sync/syncmap" 15 | ) 16 | 17 | type MockStorageAdapter struct { 18 | storage syncmap.Map 19 | url syncmap.Map 20 | } 21 | 22 | func (s *MockStorageAdapter) Upload(name string, content io.Reader) (string, error) { 23 | // name is used to only file name 24 | // actual key url is generated by object storage 25 | b, err := ioutil.ReadAll(content) 26 | if err != nil { 27 | return "", err 28 | } 29 | if len(b) == 0 { 30 | return "", usecases.ErrEmptyContent 31 | } 32 | 33 | s.storage.Store(name, b) 34 | 35 | url := fmt.Sprintf("https://example.com/%s", utils.NewUUID()) 36 | s.url.Store(url, name) 37 | return url, nil 38 | } 39 | 40 | func (s *MockStorageAdapter) Download(url string) (io.ReadCloser, error) { 41 | v, ok := s.url.Load(url) 42 | if !ok { 43 | return nil, repositories.ErrNotFound 44 | } 45 | url, ok = v.(string) 46 | if !ok { 47 | return nil, errors.New("type assertion failed") 48 | } 49 | 50 | v, ok = s.storage.Load(url) 51 | if !ok { 52 | return nil, repositories.ErrNotFound 53 | } 54 | r, ok := v.([]byte) 55 | if !ok { 56 | return nil, errors.New("type assertion failed") 57 | } 58 | return ioutil.NopCloser(bytes.NewBuffer(r)), nil 59 | } 60 | 61 | func NewMockStorage() *MockStorageAdapter { 62 | return &MockStorageAdapter{} 63 | } 64 | -------------------------------------------------------------------------------- /server/adapters/storages/mock_test.go: -------------------------------------------------------------------------------- 1 | package storages 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ktr0731/cris/server/usecases" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestMockStorageAdapter_Upload(t *testing.T) { 15 | t.Run("normal", func(t *testing.T) { 16 | storage := NewMockStorage() 17 | key := "kurisu" 18 | expected := strings.NewReader("kurisu makise") 19 | _, err := storage.Upload(key, expected) 20 | require.NoError(t, err) 21 | v, ok := storage.storage.Load(key) 22 | require.True(t, ok) 23 | actual, ok := v.(*strings.Reader) 24 | require.True(t, ok) 25 | assert.Equal(t, expected, actual) 26 | }) 27 | 28 | t.Run("empty content", func(t *testing.T) { 29 | storage := NewMockStorage() 30 | key := "sern" 31 | expected := strings.NewReader("") 32 | _, err := storage.Upload(key, expected) 33 | assert.Equal(t, usecases.ErrEmptyContent, err) 34 | }) 35 | } 36 | 37 | func TestMockStorageAdapter_Download(t *testing.T) { 38 | storage := NewMockStorage() 39 | key := "kurisu" 40 | expected := strings.NewReader("kurisu makise") 41 | url, err := storage.Upload(key, expected) 42 | v, err := storage.Download(url) 43 | require.NoError(t, err) 44 | actual, ok := v.(io.Reader) 45 | require.True(t, ok) 46 | assert.Equal(t, ioutil.NopCloser(expected), actual) 47 | } 48 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/kelseyhightower/envconfig" 4 | 5 | type Config struct { 6 | Meta *Meta 7 | Server *Server 8 | Logger *Logger 9 | } 10 | 11 | type Meta struct { 12 | Version string `default:"v1"` 13 | } 14 | 15 | type Server struct { 16 | Host string `default:""` 17 | Port string `default:"8080"` 18 | } 19 | 20 | type Logger struct { 21 | Output string `default:"stdout"` 22 | Prefix string `default:"[cris] "` 23 | } 24 | 25 | var config Config 26 | 27 | func init() { 28 | err := envconfig.Process("cris", &config) 29 | if err != nil { 30 | panic(err) 31 | } 32 | } 33 | 34 | func Get() *Config { 35 | return &config 36 | } 37 | -------------------------------------------------------------------------------- /server/db/schema.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS cris; 2 | CREATE DATABASE cris CHARACTER SET utf8mb4; 3 | 4 | USE cris; 5 | 6 | CREATE TABLE urls ( 7 | uuid CHAR(64) PRIMARY KEY, 8 | url VARCHAR(255) NOT NULL UNIQUE 9 | ) ENGINE=InnoDB; 10 | -------------------------------------------------------------------------------- /server/domain/entities/file.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import "github.com/ktr0731/cris/server/utils" 4 | 5 | type FileID string 6 | 7 | type File struct { 8 | ID FileID 9 | URL string 10 | } 11 | 12 | func NewFile(url string) *File { 13 | return &File{ 14 | ID: FileID(utils.NewUUID()), 15 | URL: url, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/domain/repositories/errors.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotFound = errors.New("specified entity not found") 7 | ) 8 | -------------------------------------------------------------------------------- /server/domain/repositories/file.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import "github.com/ktr0731/cris/server/domain/entities" 4 | 5 | type FileRepository interface { 6 | Store(*entities.File) (entities.FileID, error) 7 | Find(entities.FileID) (*entities.File, error) 8 | Remove(entities.FileID) (*entities.File, error) 9 | } 10 | -------------------------------------------------------------------------------- /server/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/ktr0731/cris/server/config" 10 | ) 11 | 12 | type Logger struct { 13 | *log.Logger 14 | out io.Writer 15 | } 16 | 17 | var logger *Logger 18 | 19 | func NewLogger(config *config.Config) (*Logger, error) { 20 | if logger != nil { 21 | return logger, nil 22 | } 23 | 24 | var out io.Writer 25 | switch config.Logger.Output { 26 | case "stdout": 27 | out = os.Stdout 28 | case "stderr": 29 | out = os.Stderr 30 | case "file": 31 | f, err := os.Create(string(time.Now().Unix())) 32 | if err != nil { 33 | return nil, err 34 | } 35 | out = f 36 | } 37 | return &Logger{ 38 | log.New(out, config.Logger.Prefix, log.Lshortfile|log.Ltime), 39 | out, 40 | }, nil 41 | } 42 | 43 | func (l *Logger) Close() error { 44 | if f, ok := l.out.(io.Closer); ok { 45 | return f.Close() 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ktr0731/cris/server/adapters/blockchains" 5 | "github.com/ktr0731/cris/server/adapters/crypto" 6 | "github.com/ktr0731/cris/server/adapters/repositories" 7 | "github.com/ktr0731/cris/server/adapters/servers" 8 | "github.com/ktr0731/cris/server/adapters/storages" 9 | "github.com/ktr0731/cris/server/config" 10 | "github.com/ktr0731/cris/server/log" 11 | "github.com/ktr0731/cris/server/presenters" 12 | "github.com/ktr0731/cris/server/usecases" 13 | ) 14 | 15 | func main() { 16 | config := config.Get() 17 | logger, err := log.NewLogger(config) 18 | if err != nil { 19 | panic(err) 20 | } 21 | defer logger.Close() 22 | 23 | interactor := usecases.NewInteractor( 24 | logger, 25 | config, 26 | presenters.NewHTTPPresenter(logger, config), 27 | storages.NewMockStorage(), 28 | blockchains.NewMockBlockchain(), 29 | crypto.NewCryptoAdapter(), 30 | repositories.NewMockFileRepository(logger, config), 31 | ) 32 | 33 | if err := servers.NewHTTPServer(logger, config, interactor).Listen(); err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server/presenters/http.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/domain/entities" 9 | "github.com/ktr0731/cris/server/log" 10 | "github.com/ktr0731/cris/server/usecases/ports" 11 | ) 12 | 13 | type HTTPPresenter struct { 14 | logger *log.Logger 15 | config *config.Config 16 | } 17 | 18 | func NewHTTPPresenter(logger *log.Logger, config *config.Config) *HTTPPresenter { 19 | return &HTTPPresenter{ 20 | logger: logger, 21 | config: config, 22 | } 23 | } 24 | 25 | func (p *HTTPPresenter) UploadFile(token entities.FileID) (*ports.UploadFileResponse, error) { 26 | return &ports.UploadFileResponse{ 27 | Token: string(token), 28 | }, nil 29 | } 30 | 31 | func (p *HTTPPresenter) DownloadFile(content io.Reader) (*ports.DownloadFileResponse, error) { 32 | res := &ports.DownloadFileResponse{} 33 | var err error 34 | res.Content, err = ioutil.ReadAll(content) 35 | return res, err 36 | } 37 | -------------------------------------------------------------------------------- /server/presenters/http_test.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/domain/entities" 9 | "github.com/ktr0731/cris/server/log" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestHTTPPresenter_UploadFile(t *testing.T) { 15 | presenter := NewHTTPPresenter(&log.Logger{}, &config.Config{}) 16 | 17 | expectedToken := entities.FileID("token") 18 | res, err := presenter.UploadFile(expectedToken) 19 | require.NoError(t, err) 20 | assert.Equal(t, res.Token, string(expectedToken)) 21 | } 22 | 23 | func TestHTTPPresenter_DownloadFile(t *testing.T) { 24 | presenter := NewHTTPPresenter(&log.Logger{}, &config.Config{}) 25 | 26 | expected := []byte("foo") 27 | r := bytes.NewReader(expected) 28 | res, err := presenter.DownloadFile(r) 29 | require.NoError(t, err) 30 | assert.Equal(t, res.Content, expected) 31 | } 32 | -------------------------------------------------------------------------------- /server/usecases/dto/transaction.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type Transaction struct { 4 | HashedData string 5 | } 6 | -------------------------------------------------------------------------------- /server/usecases/errors.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import "errors" 4 | 5 | type ClientError interface { 6 | IsClientError() bool 7 | } 8 | 9 | type errEmptyContent struct{ error } 10 | 11 | func (e errEmptyContent) IsClientError() bool { return true } 12 | 13 | type errTemperingDetected struct{ error } 14 | 15 | func (e errTemperingDetected) IsClientError() bool { return true } 16 | 17 | var ( 18 | ErrEmptyContent = errEmptyContent{errors.New("received content length is zero")} 19 | ErrTemperingDetected = errTemperingDetected{errors.New("tempering of the resource was detected")} 20 | ) 21 | -------------------------------------------------------------------------------- /server/usecases/interactor.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "github.com/ktr0731/cris/server/config" 5 | "github.com/ktr0731/cris/server/domain/repositories" 6 | "github.com/ktr0731/cris/server/log" 7 | "github.com/ktr0731/cris/server/usecases/ports" 8 | ) 9 | 10 | // interactor はすべての依存を解決する役割を持つ 11 | type Interactor struct { 12 | container *container 13 | 14 | outputPort ports.ServerOutputPort 15 | storagePort ports.StoragePort 16 | cryptoPort ports.CryptoPort 17 | fileRepository repositories.FileRepository 18 | } 19 | 20 | func (i *Interactor) UploadFile(params *ports.UploadFileParams) (*ports.UploadFileResponse, error) { 21 | return i.container.uploadFile(params, i.outputPort, i.storagePort, i.cryptoPort, i.fileRepository) 22 | } 23 | 24 | // DownloadFile downloads a file specified by the token 25 | func (i *Interactor) DownloadFile(params *ports.DownloadFileParams) (*ports.DownloadFileResponse, error) { 26 | return i.container.downloadFile(params, i.outputPort, i.storagePort, nil, i.cryptoPort, i.fileRepository) 27 | } 28 | 29 | func NewInteractor( 30 | logger *log.Logger, 31 | config *config.Config, 32 | outputPort ports.ServerOutputPort, 33 | storagePort ports.StoragePort, 34 | blockchainPort ports.BlockchainPort, 35 | cryptoPort ports.CryptoPort, 36 | fileRepository repositories.FileRepository, 37 | ) *Interactor { 38 | return &Interactor{ 39 | container: newUsecaseContainer(logger, config), 40 | 41 | outputPort: outputPort, 42 | storagePort: storagePort, 43 | cryptoPort: cryptoPort, 44 | fileRepository: fileRepository, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/usecases/ports/blockchain.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "github.com/ktr0731/cris/server/usecases/dto" 4 | 5 | type BlockchainPort interface { 6 | FindTxByHash(string) (*dto.Transaction, error) 7 | } 8 | -------------------------------------------------------------------------------- /server/usecases/ports/crypto.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | type CryptoPort interface { 4 | HashDigest(src []byte) string 5 | Verify(pubkey, msg, signature []byte) bool 6 | } 7 | -------------------------------------------------------------------------------- /server/usecases/ports/server.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/ktr0731/cris/server/domain/entities" 7 | ) 8 | 9 | type ServerInputPort interface { 10 | UploadFile(*UploadFileParams) (*UploadFileResponse, error) 11 | DownloadFile(*DownloadFileParams) (*DownloadFileResponse, error) 12 | } 13 | 14 | type UploadFileParams struct { 15 | Content io.Reader 16 | Signature []byte 17 | PubKey []byte 18 | } 19 | 20 | type DownloadFileParams struct { 21 | Token entities.FileID 22 | TxHash string 23 | } 24 | 25 | type ServerOutputPort interface { 26 | UploadFile(token entities.FileID) (*UploadFileResponse, error) 27 | DownloadFile(content io.Reader) (*DownloadFileResponse, error) 28 | } 29 | 30 | type UploadFileResponse struct { 31 | Token string `json:"token"` 32 | } 33 | 34 | type DownloadFileResponse struct { 35 | Content []byte `json:"content"` 36 | } 37 | -------------------------------------------------------------------------------- /server/usecases/ports/storage.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "io" 4 | 5 | type StoragePort interface { 6 | Upload(name string, content io.Reader) (string, error) 7 | Download(name string) (io.ReadCloser, error) 8 | } 9 | -------------------------------------------------------------------------------- /server/usecases/usecases.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | 7 | "github.com/ktr0731/cris/server/config" 8 | "github.com/ktr0731/cris/server/domain/entities" 9 | "github.com/ktr0731/cris/server/domain/repositories" 10 | "github.com/ktr0731/cris/server/log" 11 | "github.com/ktr0731/cris/server/usecases/ports" 12 | "github.com/ktr0731/cris/server/utils" 13 | ) 14 | 15 | type container struct { 16 | config *config.Config 17 | logger *log.Logger 18 | } 19 | 20 | // uploadFile uploads received file to an object storage 21 | func (c *container) uploadFile( 22 | params *ports.UploadFileParams, 23 | outputPort ports.ServerOutputPort, 24 | storagePort ports.StoragePort, 25 | cryptoPort ports.CryptoPort, 26 | fileRepository repositories.FileRepository, 27 | ) (*ports.UploadFileResponse, error) { 28 | // 雑 29 | content, err := ioutil.ReadAll(params.Content) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if !cryptoPort.Verify(params.PubKey, content, params.Signature) { 35 | return nil, ErrTemperingDetected 36 | } 37 | 38 | url, err := storagePort.Upload(utils.NewUUID(), bytes.NewBuffer(content)) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | file := entities.NewFile(url) 44 | _, err = fileRepository.Store(file) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return outputPort.UploadFile(file.ID) 50 | } 51 | 52 | func (c *container) downloadFile( 53 | params *ports.DownloadFileParams, 54 | outputPort ports.ServerOutputPort, 55 | storagePort ports.StoragePort, 56 | blockchainPort ports.BlockchainPort, 57 | cryptoPort ports.CryptoPort, 58 | fileRepository repositories.FileRepository, 59 | ) (*ports.DownloadFileResponse, error) { 60 | // tx, err := blockchainPort.FindTxByHash(params.TxHash) 61 | // if err != nil { 62 | // return nil, err 63 | // } 64 | 65 | file, err := fileRepository.Find(params.Token) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | res, err := storagePort.Download(file.URL) 71 | if err != nil { 72 | return nil, err 73 | } 74 | defer res.Close() 75 | 76 | // _, err := ioutil.ReadAll(res) 77 | // if err != nil { 78 | // return nil, err 79 | // } 80 | 81 | // if cryptoPort.HashDigest(b) != tx.HashedData { 82 | // return nil, ErrTemperingDetected 83 | // } 84 | 85 | return outputPort.DownloadFile(res) 86 | } 87 | 88 | func newUsecaseContainer(logger *log.Logger, config *config.Config) *container { 89 | return &container{ 90 | logger: logger, 91 | config: config, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /server/usecases/usecases_test.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ktr0731/cris/server/adapters/blockchains" 10 | "github.com/ktr0731/cris/server/adapters/repositories" 11 | "github.com/ktr0731/cris/server/domain/entities" 12 | "github.com/ktr0731/cris/server/usecases/ports" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | type dummyOutputAdapter struct { 18 | ports.ServerOutputPort 19 | t *testing.T 20 | } 21 | 22 | func (a *dummyOutputAdapter) UploadFile(token entities.FileID) (*ports.UploadFileResponse, error) { 23 | return &ports.UploadFileResponse{Token: string(token)}, nil 24 | } 25 | 26 | func (a *dummyOutputAdapter) DownloadFile(content io.Reader) (*ports.DownloadFileResponse, error) { 27 | b, err := ioutil.ReadAll(content) 28 | require.NoError(a.t, err) 29 | return &ports.DownloadFileResponse{Content: b}, nil 30 | } 31 | 32 | type dummyStorage struct{} 33 | 34 | func (s *dummyStorage) Upload(name string, content io.Reader) (string, error) { 35 | return "echelon", nil 36 | } 37 | 38 | func (s *dummyStorage) Download(url string) (io.ReadCloser, error) { 39 | return ioutil.NopCloser(strings.NewReader("kurisu")), nil 40 | } 41 | 42 | type dummyCryptoAdapter struct{} 43 | 44 | func (a *dummyCryptoAdapter) HashDigest(src []byte) string { 45 | return string(src) 46 | } 47 | 48 | func Test_container_uploadFile(t *testing.T) { 49 | c := newUsecaseContainer(nil, nil) 50 | output := &dummyOutputAdapter{t: t} 51 | storage := &dummyStorage{} 52 | repo := repositories.NewMockFileRepository(nil, nil) 53 | 54 | t.Run("normal", func(t *testing.T) { 55 | input := &ports.UploadFileParams{} 56 | res, err := c.uploadFile(input, output, storage, repo) 57 | require.NoError(t, err) 58 | expected, err := repo.Find(entities.FileID(res.Token)) 59 | require.NoError(t, err) 60 | assert.Equal(t, expected.URL, "echelon") 61 | }) 62 | } 63 | 64 | func Test_container_downloadFile(t *testing.T) { 65 | c := newUsecaseContainer(nil, nil) 66 | output := &dummyOutputAdapter{t: t} 67 | storage := &dummyStorage{} 68 | bc := blockchains.NewMockBlockchain() 69 | repo := repositories.NewMockFileRepository(nil, nil) 70 | 71 | t.Run("no such transaction", func(t *testing.T) { 72 | input := &ports.DownloadFileParams{} 73 | _, err := c.downloadFile(input, nil, nil, bc, nil, nil) 74 | assert.Equal(t, blockchains.ErrTxNotFound, err) 75 | }) 76 | 77 | t.Run("normal", func(t *testing.T) { 78 | input := &ports.DownloadFileParams{Token: "hououin", TxHash: "makise"} 79 | crypto := &dummyCryptoAdapter{} 80 | 81 | // setup 82 | _, err := repo.Store(&entities.File{ID: entities.FileID("hououin")}) 83 | require.NoError(t, err) 84 | _, err = bc.CreateTx("makise", strings.NewReader("kurisu")) 85 | require.NoError(t, err) 86 | 87 | // check 88 | _, err = c.downloadFile(input, output, storage, bc, crypto, repo) 89 | require.NoError(t, err) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /server/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | func NewUUID() string { 10 | uuid := uuid.NewV4().String() 11 | return strings.Replace(uuid, "-", "", 4) 12 | } 13 | -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-es3-member-expression-literals"] 3 | } 4 | -------------------------------------------------------------------------------- /web/contracts/Cris.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | 3 | contract Cris { 4 | string public version = '0.1.0'; 5 | 6 | address public admin; 7 | 8 | // mapping (string => address) fileOwner; 9 | mapping (string => bool) hasFile; 10 | 11 | function Cris() public { 12 | admin = msg.sender; 13 | } 14 | 15 | function store(string fileHash) { 16 | // fileOwner[hash] = msg.sender; 17 | hasFile[fileHash] = true; 18 | } 19 | 20 | // function isOwner(address sender, string hash) public returns (bool) { 21 | // if (sender == fileOwner[hash]) { 22 | // return true; 23 | // } 24 | // return false; 25 | // } 26 | 27 | function has(string fileHash) constant returns (bool retVal) { 28 | return hasFile[fileHash]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) { 2 | deployer.deploy(Migrations); 3 | }; 4 | -------------------------------------------------------------------------------- /web/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) { 2 | deployer.deploy(Cris); 3 | }; 4 | -------------------------------------------------------------------------------- /web/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "babel-plugin-transform-es3-member-expression-literals": { 8 | "version": "6.22.0", 9 | "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz", 10 | "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=", 11 | "dev": true, 12 | "requires": { 13 | "babel-runtime": "6.26.0" 14 | } 15 | }, 16 | "babel-runtime": { 17 | "version": "6.26.0", 18 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 19 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 20 | "dev": true, 21 | "requires": { 22 | "core-js": "2.5.1", 23 | "regenerator-runtime": "0.11.0" 24 | } 25 | }, 26 | "core-js": { 27 | "version": "2.5.1", 28 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", 29 | "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=", 30 | "dev": true 31 | }, 32 | "regenerator-runtime": { 33 | "version": "0.11.0", 34 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", 35 | "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==", 36 | "dev": true 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "setup": "./scripts/setup.sh", 8 | "start": "webpack-dev-server" 9 | }, 10 | "dependencies": { 11 | "arraybuffer-to-buffer": "^0.0.4", 12 | "arraybuffer-to-string": "^1.0.1", 13 | "babel-core": "^6.26.0", 14 | "babel-loader": "^7.1.2", 15 | "babel-preset-es2015-riot": "^1.1.0", 16 | "clipboard": "^1.7.1", 17 | "js-base64": "^2.4.0", 18 | "js-sha256": "^0.8.0", 19 | "password-generator": "^2.2.0", 20 | "riot": "^3.7.4", 21 | "riot-mui": "^0.1.1", 22 | "riot-tag-loader": "^1.0.0", 23 | "supercop.js": "^2.0.1", 24 | "truffle-solidity-loader": "^0.0.8", 25 | "web3": "^0.17.0-alpha", 26 | "webpack": "^3.10.0", 27 | "webpack-dev-server": "^2.9.5" 28 | }, 29 | "devDependencies": { 30 | "babel-plugin-transform-es3-member-expression-literals": "^6.22.0", 31 | "prettier": "1.9.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/public/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | body { 9 | background: #fafafa; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | } 15 | 16 | app { 17 | height: 100%; 18 | width: 100%; 19 | } 20 | 21 | #container { 22 | height: 100%; 23 | width: 700px; 24 | margin: 0 auto; 25 | } 26 | 27 | .uploader-wrapper p { 28 | display: block; 29 | text-align: center; 30 | vertical-align: center; 31 | padding: 100px 20px; 32 | border: solid 1px #ccc; 33 | background: #efefef; 34 | } 35 | -------------------------------------------------------------------------------- /web/public/css/riot-mui.css: -------------------------------------------------------------------------------- 1 | material-button{border:none;border-radius:2px;display:inline-block;position:relative;height:40px;line-height:36px;background:#61bdcc;color:#fff;padding:0 2rem;cursor:pointer;text-transform:uppercase;vertical-align:middle;outline:0;-webkit-tap-highlight-color:transparent}material-button:hover material-waves{background:hsla(0,0%,100%,.2)}material-button:hover material-waves,material-button material-waves{-webkit-transition:background .2s ease-in;-ms-transition:background .2s ease-in;-moz-transition:background .2s ease-in;-o-transition:background .2s ease-in;transition:background .2s ease-in}material-button material-waves{background:hsla(0,0%,100%,0)}material-button .content{width:101%;height:100%;display:block;text-align:center}material-button .content .text,material-button .content a,material-button .content i.icon,material-button .content i.material-icons{display:inline-block;vertical-align:middle;font-size:18px;color:#fff;line-height:40px}material-button .content .text.material-icons,material-button .content a.material-icons,material-button .content i.icon.material-icons,material-button .content i.material-icons.material-icons{font-size:20px}material-button .content .text svg,material-button .content a svg,material-button .content i.icon svg,material-button .content i.material-icons svg{fill:#fff;stroke:#fff}material-button[rounded=true]{border-radius:50%;width:40px;padding:0}material-button[rounded=true] .content{width:100%;height:100%;display:flex;align-items:center;text-align:center}material-button[rounded=true] .content i.icon,material-button[rounded=true] .content i.material-icons{display:inline-block;text-align:center;width:100%;height:100%;-webkit-user-select:none;-ms-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none}material-button[shady=true]{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transition:box-shadow .2s}material-button[shady=true]:hover{-webkit-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);box-shadow:0 2px 7px 0 rgba(0,0,0,.14),0 2px 12px 0 rgba(0,0,0,.12);transition:box-shadow .2s}material-button[disabled=true]{background:#ccc;color:#999;cursor:default}material-button[disabled=true] #content .text,material-button[disabled=true] #content a,material-button[disabled=true] #content i.icon,material-button[disabled=true] #content i.material-icons{color:#999}material-button[disabled=true] #content .text svg,material-button[disabled=true] #content a svg,material-button[disabled=true] #content i.icon svg,material-button[disabled=true] #content i.material-icons svg{fill:#999;stroke:#999}material-button[disabled=true]:hover material-waves{background:transparent}material-card{display:block;background-color:#fff;margin:0;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-card .title{padding:20px 10px;font-size:22px;color:#25313b;-webkit-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);-ms-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);-moz-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);-o-box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02);box-shadow:0 2px 3px 0 rgba(0,0,0,.08),0 2px 7px 0 rgba(0,0,0,.02)}material-card .material-card-content{padding:20px}material-checkbox{display:block;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);-moz-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}material-checkbox,material-checkbox .checkbox{background-color:transparent;position:relative}material-checkbox .checkbox{display:inline-block;box-sizing:border-box;height:100%;border:2px solid;border-color:#25313b;border-radius:2px;width:18px;height:18px;cursor:pointer;vertical-align:middle;-webkit-transition:background-color .14s,border-color .14s;-ms-transition:background-color .14s,border-color .14s;-moz-transition:background-color .14s,border-color .14s;-o-transition:background-color .14s,border-color .14s;transition:background-color .14s,border-color .14s}material-checkbox .checkbox .checkmark{-webkit-transform:rotate(0deg) scale(.5);-ms-transform:rotate(0deg) scale(.5);-moz-transform:rotate(0deg) scale(.5);-o-transform:rotate(0deg) scale(.5);transform:rotate(0deg) scale(.5);position:absolute;top:-1px;left:3px;width:6px;height:10px;border-style:solid;border-top:none;border-left:none;border-right-width:2px;border-bottom-width:2px;border-color:transparent;cursor:pointer}material-checkbox .checkbox.checked{background-color:#25313b}material-checkbox .checkbox.checked,material-checkbox .checkbox.checked .checkmark{-webkit-transition:background-color .14s,border-color .14s,transform .14s cubic-bezier(.23,1,.32,1) 50ms;-ms-transition:background-color .14s,border-color .14s,transform .14s 50ms cubic-bezier(.23,1,.32,1);-moz-transition:background-color .14s,border-color .14s,transform .14s 50ms cubic-bezier(.23,1,.32,1);-o-transition:background-color .14s,border-color .14s,transform .14s 50ms cubic-bezier(.23,1,.32,1);transition:background-color .14s,border-color .14s,transform .14s cubic-bezier(.23,1,.32,1) 50ms}material-checkbox .checkbox.checked .checkmark{border-color:#fff;-webkit-transform:rotate(45deg) scale(1);-ms-transform:rotate(45deg) scale(1);-moz-transform:rotate(45deg) scale(1);-o-transform:rotate(45deg) scale(1);transform:rotate(45deg) scale(1)}material-checkbox .label{color:#25313b;position:relative;display:inline-block;vertical-align:middle;padding-left:8px;white-space:normal;cursor:pointer}material-checkbox[disabled=true] .checkbox{border-color:#ccc}material-checkbox[disabled=true] .label{color:#ccc}material-combo{display:block;position:relative}material-combo [hidden]{display:none}material-combo material-input input{color:#2f6975}material-dropdown{position:absolute;z-index:100;width:100%}material-dropdown .dropdown{background-color:#fff;margin:0;min-width:100px;max-height:650px;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-dropdown .dropdown.opening.top{transform:translateY(-50px)}material-dropdown .dropdown.opening.bottom,material-dropdown .dropdown.opening.top{opacity:0;-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-dropdown .dropdown.opening.bottom{transform:translateY(50px)}material-dropdown-list{position:absolute;z-index:100;width:100%}material-dropdown-list ul.dropdown-content{list-style:none;z-index:100;background-color:#fff;margin:0;padding:0;min-width:100px;max-height:650px;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1}material-dropdown-list ul.dropdown-content,material-dropdown-list ul.dropdown-content.opening{-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-dropdown-list ul.dropdown-content.opening{transform:translateY(-50px);opacity:0}material-dropdown-list ul.dropdown-content li{clear:both;cursor:pointer;line-height:1.5rem;width:100%;text-align:left;text-transform:none;background-color:#fff;-webkit-transition:background-color .2s ease-in;-ms-transition:background-color .2s ease-in;-moz-transition:background-color .2s ease-in;-o-transition:background-color .2s ease-in;transition:background-color .2s ease-in}material-dropdown-list ul.dropdown-content li a,material-dropdown-list ul.dropdown-content li span{font-size:1.2rem;color:#25313b;display:block;padding:1rem}material-dropdown-list ul.dropdown-content li:hover{background-color:#ededed;-webkit-transition:background-color .2s ease-out;-ms-transition:background-color .2s ease-out;-moz-transition:background-color .2s ease-out;-o-transition:background-color .2s ease-out;transition:background-color .2s ease-out}material-dropdown-list ul.dropdown-content li a{text-decoration:none;color:#25313b}material-dropdown-list ul.dropdown-content li.selected{background-color:#394b5a;-webkit-transition:background-color .2s ease-out;-ms-transition:background-color .2s ease-out;-moz-transition:background-color .2s ease-out;-o-transition:background-color .2s ease-out;transition:background-color .2s ease-out}material-dropdown-list ul.dropdown-content li.selected span{color:#fff}.material-dropdown-list-overlay{z-index:99;width:100%;height:100%;top:0;left:0;position:fixed;background:transparent}material-input{display:block;padding:8px 0;position:relative}material-input .label-placeholder{height:15px;width:100%}material-input .input-content{font-size:16px;color:#17242e;position:relative}material-input .input-content label{position:absolute;top:0;right:0;left:0;font:inherit;color:#2f6975;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-size:16px;font-weight:400;line-height:24px;-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-input .input-content.not-empty label,material-input .input-content label{-webkit-transition:transform .2s;-ms-transition:transform .2s;-moz-transition:transform .2s;-o-transition:transform .2s;transition:transform .2s}material-input .input-content.not-empty label{-webkit-transform:translate3d(0,-70%,0) scale(.7);-ms-transform:translate3d(0,-70%,0) scale(.7);-moz-transform:translate3d(0,-70%,0) scale(.7);-o-transform:translate3d(0,-70%,0) scale(.7);transform:translate3d(0,-70%,0) scale(.7);-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-input .input-content input{position:relative;outline:none;box-shadow:none;padding:0;width:100%;background:transparent;border:none;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-weight:400;line-height:24px;height:24px}material-input .input-content .iconWrapper{display:inline-block;position:absolute;top:0;left:0;bottom:0;right:0;width:40px;height:40px;margin-left:-33px;left:100%;margin-top:-7px}material-input .input-content .iconWrapper material-button{background:transparent}material-input .input-content .iconWrapper material-button .content .material-icons{color:#2f6975}material-input .underline{position:relative;display:block}material-input .underline .unfocused-line{height:1px;background:#2f6975}material-input .underline .focused-line{height:2px;background:#2f6975;-webkit-transform:scaleX(0);-ms-transform:scaleX(0);-moz-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0);-webkit-transition:transform .2s ease-in;-ms-transition:transform .2s ease-in;-moz-transition:transform .2s ease-in;-o-transition:transform .2s ease-in;transition:transform .2s ease-in}material-input .underline.focused .focused-line{-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transition:transform .2s ease-out;-ms-transition:transform .2s ease-out;-moz-transition:transform .2s ease-out;-o-transition:transform .2s ease-out;transition:transform .2s ease-out}material-input .underline.error .focused-line,material-input .underline.error .unfocused-line{background:#941212;-webkit-transition:background .2s ease-out;-ms-transition:background .2s ease-out;-moz-transition:background .2s ease-out;-o-transition:background .2s ease-out;transition:background .2s ease-out}material-input[disabled=true] label{color:#ccc}material-input[disabled=true] .underline .unfocused-line{background:#ccc}material-navbar{display:block;color:#fff;-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);-ms-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);-moz-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);-o-box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);box-shadow:0 8px 17px 0 rgba(0,0,0,.12),0 6px 20px 0 rgba(0,0,0,.14);background-color:#25313b;width:100%;height:70px;line-height:70px}material-navbar:not(material-input){line-height:0}material-navbar a{color:#fff}material-navbar .nav-wrapper{position:relative;height:100%}material-navbar .nav-wrapper .logo{line-height:66px}material-navbar[fixed]{position:relative;height:70px;z-index:998}material-pane{display:block}material-pane material-navbar .nav-wrapper{display:flex;justify-content:space-between;flex-grow:1}material-pane material-navbar .nav-wrapper .material-pane-title{font-size:26px;text-transform:uppercase;text-align:center}material-pane material-navbar .nav-wrapper material-button{background:transparent}material-pane .content .material-pane-content{padding:10px;background:#f2f2f2}material-popup .popup{z-index:100;position:fixed;left:0;top:25%;right:0;width:50%;min-width:300px;margin:auto;height:-webkit-fit-content;height:-ms-fit-content;height:-moz-fit-content;height:-o-fit-content;height:fit-content;background-color:#fff;overflow-y:auto;will-change:width,height;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-popup .popup.opening.top{transform:translateY(-50px)}material-popup .popup.opening.bottom,material-popup .popup.opening.top{opacity:0;-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-popup .popup.opening.bottom{transform:translateY(50px)}material-popup .popup>.content{padding:10px;color:#25313b}material-popup .popup>.content .material-popup-title{padding:24px 24px 0;margin:0;font-size:2.5rem}material-popup .popup>.content .material-popup-content{padding:20px 24px 24px;margin:0}material-popup .popup>.content .material-popup-action{padding:8px 8px 8px 24px;margin:0;display:flex;flex-direction:row-reverse;flex-wrap:wrap}material-popup .popup>.content .material-popup-action>*{margin-right:8px}material-popup .popup>.content .material-popup-action>:first-child{margin-right:0}material-popup .overlay{z-index:99;width:100%;height:100%;top:0;left:0;position:fixed;background:rgba(0,0,0,.2)}material-snackbar{display:block;position:fixed;bottom:10%;right:7%;z-index:1001}material-snackbar .toast{-webkit-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-o-border-radius:3px;border-radius:3px;top:0;width:auto;clear:both;margin-top:10px;position:relative;height:40px;line-height:40px;background-color:#25313b;padding:0 25px;font-size:1.1rem;font-weight:300;color:#fff;z-index:1001;-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-ms-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);-o-box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;-webkit-justify-content:space-between;-ms-justify-content:space-between;-moz-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;transform:translateY(0);opacity:1;-webkit-transition:transform .2s ease-in,opacity .2s;-ms-transition:transform .2s ease-in,opacity .2s;-moz-transition:transform .2s ease-in,opacity .2s;-o-transition:transform .2s ease-in,opacity .2s;transition:transform .2s ease-in,opacity .2s}material-snackbar .toast.opening{transform:translateY(-15px);opacity:0;-webkit-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-ms-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-moz-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;-o-transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s;transition:transform .2s cubic-bezier(.23,1,.32,1),opacity .2s}material-snackbar .toast.error{background-color:#ab173e}material-spinner{display:inline-block}material-spinner .loader-circular{-webkit-animation:loader-rotate 2s linear infinite;-ms-animation:loader-rotate 2s linear infinite;-moz-animation:loader-rotate 2s linear infinite;-o-animation:loader-rotate 2s linear infinite;animation:loader-rotate 2s linear infinite}material-spinner .loader-path{stroke-dasharray:1,200;stroke-dashoffset:0;-webkit-animation:dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite,color 6s ease-in-out infinite;stroke-linecap:round;stroke:#db652d}@-o-keyframes loader-rotate{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(1turn)}}@-moz-keyframes loader-rotate{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(1turn)}}@-webkit-keyframes loader-rotate{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes loader-rotate{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:89,200;stroke-dashoffset:-35}to{stroke-dasharray:89,200;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:89,200;stroke-dashoffset:-35}to{stroke-dasharray:89,200;stroke-dashoffset:-124}}@-webkit-keyframes color{0%{stroke:#3f88f8}20%{stroke:#3f88f8}25%{stroke:#dd413b}45%{stroke:#dd413b}50%{stroke:#f6ae2e}70%{stroke:#f6ae2e}75%{stroke:#259a5d}95%{stroke:#259a5d}}@keyframes color{0%{stroke:#3f88f8}20%{stroke:#3f88f8}25%{stroke:#dd413b}45%{stroke:#dd413b}50%{stroke:#f6ae2e}70%{stroke:#f6ae2e}75%{stroke:#259a5d}95%{stroke:#259a5d}}material-tabs{display:inline-block;line-height:0;position:relative;width:100%}material-tabs material-button{background:#070731;border-radius:0;text-align:center;padding:0}material-tabs material-button .content{position:relative}material-tabs material-button .content .text{font-size:16px}material-tabs material-button.selected .content .text{color:#61bdcc;-webkit-transition:color .3s;-ms-transition:color .3s;-moz-transition:color .3s;-o-transition:color .3s;transition:color .3s}material-tabs .line-wrapper{width:100%;height:3px;background:#c2c7b6;position:relative}material-tabs .line-wrapper .line{height:100%;background:#61bdcc;position:absolute;-webkit-transition:left .4s;-ms-transition:left .4s;-moz-transition:left .4s;-o-transition:left .4s;transition:left .4s}material-textarea{display:block;padding:8px 0}material-textarea .label-placeholder{height:15px;width:100%}material-textarea .textarea-content{font-size:16px;color:#17242e;position:relative}material-textarea .textarea-content label{position:absolute;top:0;right:0;left:0;font:inherit;color:#2f6975;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-size:16px;font-weight:400;line-height:24px;-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-textarea .textarea-content.not-empty label,material-textarea .textarea-content label{-webkit-transition:transform .2s;-ms-transition:transform .2s;-moz-transition:transform .2s;-o-transition:transform .2s;transition:transform .2s}material-textarea .textarea-content.not-empty label{-webkit-transform:translate3d(0,-70%,0) scale(.7);-ms-transform:translate3d(0,-70%,0) scale(.7);-moz-transform:translate3d(0,-70%,0) scale(.7);-o-transform:translate3d(0,-70%,0) scale(.7);transform:translate3d(0,-70%,0) scale(.7);-webkit-transform-origin:left top;-ms-transform-origin:left top;-moz-transform-origin:left top;-o-transform-origin:left top;transform-origin:left top}material-textarea .textarea-content .textarea-container{position:absolute;top:0;right:0;left:0;width:100%;height:100%;overflow:hidden}material-textarea .textarea-content .textarea-container textarea{position:relative;box-shadow:none;padding:0;height:100%;background:transparent;border:none;resize:none;overflow:hidden}material-textarea .textarea-content .mirror,material-textarea .textarea-content .textarea-container textarea{outline:none;width:100%;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-weight:400;line-height:24px}material-textarea .textarea-content .mirror{visibility:hidden;word-wrap:break-word;height:auto;min-height:24px}material-textarea .underline{position:relative;display:block}material-textarea .underline .unfocused-line{height:1px;background:#2f6975}material-textarea .underline .focused-line{height:2px;background:#2f6975;-webkit-transform:scaleX(0);-ms-transform:scaleX(0);-moz-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0);-webkit-transition:transform .2s ease-in;-ms-transition:transform .2s ease-in;-moz-transition:transform .2s ease-in;-o-transition:transform .2s ease-in;transition:transform .2s ease-in}material-textarea .underline.focused .focused-line{-webkit-transform:none;-ms-transform:none;-moz-transform:none;-o-transform:none;transform:none;-webkit-transition:transform .2s ease-out;-ms-transition:transform .2s ease-out;-moz-transition:transform .2s ease-out;-o-transition:transform .2s ease-out;transition:transform .2s ease-out}material-textarea .underline.error .focused-line,material-textarea .underline.error .unfocused-line{background:#941212;-webkit-transition:background .2s ease-out;-ms-transition:background .2s ease-out;-moz-transition:background .2s ease-out;-o-transition:background .2s ease-out;transition:background .2s ease-out}material-textarea[disabled=true] label{color:#ccc}material-textarea[disabled=true] .underline .unfocused-line{background:#ccc}material-waves{position:absolute;left:0;top:0}material-waves,material-waves #waves{display:block;width:100%;height:100%;overflow:hidden}material-waves #waves{position:relative;-webkit-user-select:none;-ms-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none;vertical-align:middle;-webkit-transform:rotate(0deg);z-index:1}material-waves #waves .wave{position:absolute;-webkit-border-radius:50%;-ms-border-radius:50%;-moz-border-radius:50%;-o-border-radius:50%;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;z-index:0;opacity:0;-webkit-transform:scale(0);-ms-transform:scale(0);-moz-transform:scale(0);-o-transform:scale(0);transform:scale(0);-webkit-transition-property:transform,opacity;-ms-transition-property:transform,opacity;-moz-transition-property:transform,opacity;-o-transition-property:transform,opacity;transition-property:transform,opacity;pointer-events:none}material-waves[rounded=true],material-waves[rounded=true] #waves{border-radius:50%}material-footer{display:block;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:16px;color:#999;background-color:#555}material-footer a{color:inherit;text-decoration:none}material-footer ul{list-style:none;margin:0;padding:0}material-footer .material-footer-sections{display:-moz-flex;display:-webkit-flex;display:-ms-flexbox;display:flex;-moz-flex-flow:wrap;-webkit-flex-flow:wrap;flex-flow:wrap;padding-bottom:24px}material-footer .material-footer-sections ul{margin-right:16px}material-footer .mini-footer{display:block}@media screen and (min-width:760px){material-footer .mini-footer{display:inline-flex}}material-footer .material-footer-logo,material-footer .material-footer-section-header{margin-bottom:16px;color:#fff;line-height:36px}@media screen and (min-width:760px){material-footer .material-footer-logo{margin-bottom:0;margin-right:16px}}material-footer ul.material-footer-link-list{display:-moz-flex;display:-webkit-flex;display:-ms-flexbox;display:flex}material-footer ul.material-footer-link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){material-footer ul.material-footer-link-list li{line-height:36px}} -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cris 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /web/public/js/riot-mui.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(n){if(i[n])return i[n].exports;var o=i[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var i={};e.m=t,e.c=i,e.i=function(t){return t},e.d=function(t,i,n){e.o(t,i)||Object.defineProperty(t,i,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(i,"a",i),i},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=38)}([function(t,e,i){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},a=function(){function t(){n(this,t)}return t.prototype.receiveBound=function(){this.container||console.error("Yor class must contain a container. It is DOM Element. Define please this.container property.");var t,e,i,n=this.container&&this.container.ownerDocument;return t=n.documentElement,"undefined"!==o(this.container.getBoundingClientRect)&&(i=this.container.getBoundingClientRect()),e=this.getWindow(n),this.mix(i,{size:Math.max(i.width,i.height),offsetTop:i.top+e.pageYOffset-t.clientTop,offsetLeft:i.left+e.pageXOffset-t.clientLeft})},t.prototype.isWindow=function(t){return null!==t&&t===t.window},t.prototype.getWindow=function(t){return this.isWindow(t)?t:9===t.nodeType&&t.defaultView},t.prototype.mix=function(t,e){for(var i in t)i in e||(e[i]=t[i]);return e},t}();e.default=a,riot.mixin("Bound",a)},function(t,e,i){"use strict";riot.tag2("material-button",'
',"","",function(t){var e=this;this.dynamicAttributes=["disabled"],this.disabled=t.disabled||!1,this.launch=function(t){e.disabled||e.refs["material-waves"].trigger("launch",t)},this.on("mount",function(){e.refs["material-waves"].on("wavestart",function(t){e.trigger("wavestart",t)}),e.refs["material-waves"].on("waveend",function(){e.trigger("waveend")})}),this.click=function(){t.link&&(window.location.href=t.link),e.trigger("click")},this.mixin("dynamicAttributes")})},function(t,e,i){"use strict";riot.tag2("material-card",'
',"","",function(t){var e=this;this.titleExist=!1,this.on("mount",function(){e.update({titleExist:!!e.root.querySelector(".material-card-title")})}),this.mixin("content")})},function(t,e,i){"use strict";riot.tag2("material-checkbox",'
',"","",function(t){var e=this;this.checked=t.checked||!1,this.disabled=t.disabled||!1,this.toggle=function(){if(e.disabled)return!1;e.update({checked:!e.checked}),e.trigger("toggle",e.checked)}})},function(module,exports,__webpack_require__){"use strict";riot.tag2("material-combo",' ',"","",function(opts){var _this=this;this.items=[],this.title=null;var lastValue=this.value,valueChanged=function(){_this.value!==lastValue&&(lastValue=_this.value,_this.root.dispatchEvent(new CustomEvent("change",{value:_this.value})))};if(this.getOptions=function(){Array.prototype.forEach.call(_this.refs.options.children,function(t,e){if("option"==t.tagName.toLowerCase()){var i={title:t.innerHTML,value:t.getAttribute("value")};_this.items.push(i),null!=t.getAttribute("isSelected")&&(_this.refs.dropdown.update({selected:e}),_this.update({value:i.value||i.title}),valueChanged(),_this.title=i.title)}}),_this.refs.dropdown.update({items:_this.items}),_this.refs.dropdown.selected&&_this.update({hValue:_this.refs.dropdown.items[_this.refs.dropdown.selected].value||_this.refs.dropdown.items[_this.refs.dropdown.selected].title}),_this.update({isParsed:!0}),valueChanged()},opts.items)try{this.items=eval(opts.items)||[],this.items.length&&this.refs.dropdown.update({items:this.items})}catch(t){console.error("Something wrong with your items. For details look at it - "+t)}this.on("mount",function(){_this.getOptions(),_this.refs.dropdown.on("selectChanged",function(t){_this.update({value:_this.refs.dropdown.items[t].value||_this.refs.dropdown.items[t].title}),valueChanged(),_this.refs.input.update({value:_this.refs.dropdown.items[t].title}),setTimeout(function(){_this.refs.dropdown.update({items:_this.items})},200)}),_this.refs.input.on("valueChanged",function(t){_this.refs.dropdown.update({items:_this.filter("items",{title:t})})}),_this.refs.input.on("focusChanged",function(t){_this.refs.input.value==(opts.defaulttext||"Choose item")&&t&&_this.refs.input.update({value:""}),""!=_this.refs.input.value||t||_this.refs.input.update({value:opts.defaulttext||"Choose item"}),t&&_this.refs.dropdown.open()}),_this.refs.dropdown.root.style.top=_this.refs.input.root.getBoundingClientRect().height+"px",_this.refs.input.update({value:_this.title||opts.defaulttext||"Choose item"})}),this.mixin("collection")})},function(module,exports,__webpack_require__){"use strict";riot.tag2("material-dropdown-list",'
',"","",function(opts){var _this=this;if(this.opened=!1,opts.items){try{this.items=eval(opts.items)||[]}catch(t){console.error("Something wrong with your items. For details look at it - "+t)}this.update({items:this.items})}opts.selected&&this.update({selected:opts.selected}),this.select=function(t){return _this.update({selected:t.item.key}),_this.close(),_this.trigger("selectChanged",t.item.key,t.item.item),!0},this.open=function(){_this.update({opened:!0,opening:!0}),_this.opts.extraclose&&document.body.appendChild(_this.refs.overlay),setTimeout(function(){_this.update({opening:!1})},0)},this.close=function(){_this.update({opening:!0}),setTimeout(function(){_this.update({opened:!1})},200)}})},function(t,e,i){"use strict";riot.tag2("material-dropdown",'
',"","",function(t){var e=this;this.opened=t.opened||!1,this.open=function(){e.update({opened:!0,opening:!0}),setTimeout(function(){e.update({opening:!1})},0)},this.close=function(){e.update({opening:!0}),setTimeout(function(){e.update({opened:!1})},200)}})},function(t,e,i){"use strict";riot.tag2("material-footer",' ',"","",function(t){this.mixin("content")})},function(t,e,i){"use strict";riot.tag2("material-input",'
',"","",function(t){var e=this;if(this.opts=t,this.required="",this.name=t.ref||"input",this.notSupportedTypes=["date","color","datetime","month","range","time"],-1!=this.notSupportedTypes.indexOf(t.type))throw new Error("Sorry but we do not support "+t.type+" type yet!");this.update({showIcon:!1}),this.on("mount",function(){e.update({value:t.value||"",disabled:t.disabled||!1,required:t.required||!1}),e.n=t.ref||"default-input",e.refs[e.n].addEventListener("focus",e.changeFocus),e.refs[e.n].addEventListener("blur",e.changeFocus)}),this.changeFocus=function(i){if(e.disabled)return!1;e.update({focused:e.refs[e.n]==document.activeElement}),e.trigger("focusChanged",e.focused,i),t.focuschanged&&"function"==typeof t.focuschanged&&t.focuschanged(e.focused)},this.changeValue=function(i){e.update({value:e.refs[e.n].value}),e.trigger("valueChanged",e.refs[e.n].value,i),t.valuechanged&&"function"==typeof t.valuechanged&&t.valuechanged(e.refs[e.n].value)},this.iconClickHandler=function(t){e.opts.iconclicked&&"function"==typeof e.opts.iconclicked&&e.opts.iconclicked.call(e,t),e.trigger("iconclicked",t)},this.on("update",function(t){t&&void 0!=t.value&&e.validationType&&e.isValid(e.validate(t.value))}),this.isValid=function(t){e.update({error:!t})},this.mixin("validate")})},function(t,e,i){"use strict";riot.tag2("material-navbar",'',"",'role="toolbar"',function(t){})},function(t,e,i){"use strict";riot.tag2("material-pane",'
',"","",function(t){this.mixin("content")})},function(t,e,i){"use strict";riot.tag2("material-popup",'
',"","",function(t){var e=this;this.opened=t.opened||!1,this.on("mount",function(){document.body.appendChild(e.root)}),this.open=function(){e.update({opened:!0,opening:!0}),setTimeout(function(){e.update({opening:!1})},0)},this.close=function(){e.update({opening:!0}),setTimeout(function(){e.update({opened:!1})},200)},this.mixin("content")})},function(t,e,i){"use strict";riot.tag2("material-snackbar",'
{toast.message}
',"","",function(t){var e=this;this.toasts=[],this.intervals={},this.addToast=function(i,n){var o=e.toastID=Math.random().toString(36).substring(7);e.toasts.push(Object.assign(i,{opening:!0,_id:o})),e.update({toasts:e.toasts}),setTimeout(function(){e.toasts[e.findToastKeyByID(o)].opening=!1,e.update({toasts:e.toasts})},50),e.intervals[o]=setTimeout(function(){e.removeToast(o)},t.duration||n||5e3)},this.removeToastAfterDurationEnding=function(t){e.removeToast(t)},this.findToastKeyByID=function(t){var i=null;return e.toasts.forEach(function(e,n){e._id==t&&(i=n)}),i},this.removeToastByClick=function(t){var i=t.item.toast._id;clearInterval(e.intervals[i]),e.removeToast(i)},this.removeToast=function(t){null!=e.findToastKeyByID(t)&&(e.toasts[e.findToastKeyByID(t)].opening=!0,e.update({toasts:e.toasts}),setTimeout(function(){e.toasts.splice(e.findToastKeyByID(t),1),e.update({toasts:e.toasts})},200))}})},function(t,e,i){"use strict";riot.tag2("material-spinner",' ',"","",function(t){})},function(module,exports,__webpack_require__){"use strict";riot.tag2("material-tabs",'
{parent.opts.cut ? parent.cut(tab.title) : tab.title}
',"","",function(opts){var _this=this;if(this.selected=0,this.tabs=[],opts.tabs){var tabs=[];try{tabs=opts.tabs?eval(opts.tabs):[],this.tabs=tabs}catch(t){}}this.on("mount",function(){_this.setWidth(),_this.setLinePosition()}),this.setWidth=function(){[].forEach.call(_this.root.querySelectorAll("material-button"),function(t){t.style.width=_this.refs.line.style.width=(100/_this.tabs.length).toFixed(2)+"%"})},this.onChangeTab=function(t){var e=_this.tabs.indexOf(t.item.tab);_this.changeTab(e)},this.changeTab=function(t){_this.update({selected:t}),_this.setLinePosition(),_this.trigger("tabChanged",_this.tabs[t],t),opts.tabchanged&&"function"==typeof opts.tabchanged&&opts.tabchanged(_this.tabs[t],t)},this.setLinePosition=function(){_this.refs.line.style.left=(100/_this.tabs.length).toFixed(2)*_this.selected+"%"},this.cut=function(t){return t.length>opts.cut?t.substr(0,opts.cut)+"...":t}})},function(t,e,i){"use strict";riot.tag2("material-textarea",'
',"","",function(t){var e=this;this.opts=t,this.disabled=t.disabled||!1,this.on("mount",function(){t.maxRows&&(e.refs.mirror.style.maxHeight=t.maxRows*e.refs[e.n].getBoundingClientRect().height+"px"),e.n=t.ref||"default-textarea",e.value=t.value||"",e.refs.mirror.innerHTML=e.format(e.value),e.refs[e.n].value=e.value,e.refs[e.n].scrollTop=e.refs[e.n].scrollHeight,e.refs[e.n].addEventListener("focus",e.changeFocus),e.refs[e.n].addEventListener("blur",e.changeFocus),e.refs[e.n].addEventListener("input",e.inputHandler),e.update({value:e.value})}),this.changeFocus=function(t){if(e.disabled)return!1;var i=e.refs[e.n]==document.activeElement;e.update({focused:i}),e.trigger("focusChanged",i)},this.inputHandler=function(t){var i=e.refs[e.n].value;e.refs.mirror.innerHTML=e.format(i),e.update({value:i})},this.changeValue=function(i){e.trigger("valueChanged",e.refs[e.n].value,i),t.valuechanged&&"function"==typeof t.valuechanged&&t.valuechanged(e.refs[e.n].value)},this.isValid=function(t){e.update({error:!t})},this.format=function(t){return t.replace(/\n/g,"
 ")},this.on("update",function(t){t&&void 0!=t.value&&e.validationType&&e.isValid(e.validate(t.value))}),this.mixin("validate")})},function(t,e,i){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function a(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var s=i(0),r=function(t){return t&&t.__esModule?t:{default:t}}(s);riot.tag2("material-waves",'
',"","",function(t){var e=this,i=function(t){function e(i,a,s){n(this,e);var r=o(this,t.call(this));return i||console.error("You should set container to the wave!"),r.container=i,r.maxOpacity=a.opacity||.6,r.duration=a.duration||750,r.color=a.color||"#fff",r.center=a.center||!1,r.event=s,r.containerBound=r.receiveBound(),r.maxScale=r.containerBound.size/100*10,r.created=Date.now(),r.start={},r.createNode(),r.waveIn(),r}return a(e,t),e.prototype.createNode=function(){this.wave=document.createElement("div"),this.wave.classList.add("wave"),this.container.appendChild(this.wave)},e.prototype.waveIn=function(){var t=this;this.center&&!this.event&&console.error("Setup at least mouse event... Or just set center attribute"),this.start.x=this.center?this.containerBound.height/2:this.event.pageY-this.containerBound.offsetTop,this.start.y=this.center?this.containerBound.width/2:this.event.pageX-this.containerBound.offsetLeft;var e=-1!==window.navigator.userAgent.indexOf("Trident");setTimeout(function(){return t.setStyles(t.maxOpacity)},e?50:0)},e.prototype.waveOut=function(t){var e=this,i=Date.now()-this.created,n=Math.round(this.duration/2)-i,o=n>0?n:0;setTimeout(function(){e.setStyles(0),setTimeout(function(){e.wave.parentNode===e.container&&(e.container.removeChild(e.wave),t())},e.duration)},o)},e.prototype.setStyles=function(t){this.wave.setAttribute("style",this.convertStyle({top:this.start.x+"px",left:this.start.y+"px",transform:"scale("+this.maxScale+")","transition-duration":this.duration+"ms","transition-timing-function":"cubic-bezier(0.250, 0.460, 0.450, 0.940)",background:this.color,opacity:t}))},e.prototype.convertStyle=function(t){var e="";return Object.keys(t).forEach(function(i){t.hasOwnProperty(i)&&(e+=i+":"+t[i]+";")}),e},e}(r.default);this._waves=[],this._events=[],this.on("launch",function(n){var o=new i(e.refs.waves,t,n);e._waves.push(o),e.trigger("wavestart",o),e.parent&&e.parent.opts&&e.parent.opts.wavestart&&e.parent.opts.wavestart(o),e._events.length||(e._events.push(n.target.addEventListener("mouseup",function(){return e.trigger("hold")})),e._events.push(n.target.addEventListener("mouseleave",function(){return e.trigger("hold")})))}),this.on("hold",function(){e._waves[e._waves.length-1]&&e._waves[e._waves.length-1].waveOut(e.waveOut),e._waves[e._waves.length-1]&&e._waves.slice(e._waves.length-1,1)}),this.waveOut=function(){e.trigger("waveend"),e.parent&&e.parent.opts&&e.parent.opts.waveend&&e.parent.opts.waveend()}})},function(t,e,i){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){n(this,t)}return t.prototype.filter=function(t,e){return this[t].filter(function(t){var i=!1;return Object.keys(e).forEach(function(n){var o=e[n],a=new RegExp(""+o,"i");i=a.test(t[n])}),i})},t.prototype.find=function(t,e){var i={},n=0;return t.forEach(function(t){Object.keys(e).forEach(function(o){var a=e[o];t[o]==a&&(i.e=t,i.k=n)}),n++}),i},t}();e.default=o,riot.mixin("collection",o)},function(t,e,i){"use strict";var n={init:function(){var t=this;this.on("mount",function(){[].forEach.call(t.root.querySelectorAll("content"),function(e){var i=e.getAttribute("select");[].forEach.call(t.root.querySelectorAll(i),function(t){e.parentNode.insertBefore(t,e.nextSibling)}),e.parentNode.removeChild(e)})})}};riot.mixin("content",n)},function(t,e,i){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){n(this,t)}return t.prototype.init=function(){var t=this;this.on("update",function(e){e&&t.dynamicAttributes&&t.dynamicAttributes.forEach(function(i){void 0!=e[i]&&t.root.setAttribute(i,e[i])})})},t}();e.default=o,riot.mixin("dynamicAttributes",o)},function(t,e,i){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var o=function(){function t(){n(this,t)}return t.prototype.findTag=function(t,e){var i=null;return t.forEach(function(t){t.root.getAttribute("name").toLowerCase()!=e.toLowerCase()&&t.root.tagName.toLowerCase()!=e.toLowerCase()&&t.root.getAttribute("ref").toLowerCase()!=e.toLowerCase()||(i=t)}),i},t.prototype.turnHyphensOptsToCamelCase=function(t){for(var e in t)if(/-/.test(e)){var i=e.replace(/-([a-z])/g,function(t){return t[1].toUpperCase()});t[i]=t[e],delete t[e]}},t}();e.default=o,riot.findTag=o.findTag,riot.mixin("helpers",o)},function(module,exports,__webpack_require__){"use strict";function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function t(t,e){for(var i=0;i { 2 | let account = { 3 | address: localStorage['address'], 4 | pubkey: localStorage['pubkey'], 5 | privkey: localStorage['privkey'] 6 | }; 7 | 8 | if (!account.address || !account.pubkey || !account.privkey) { 9 | ethClient.createAccount().then(res => { 10 | localStorage['address'] = res.address; 11 | localStorage['pubkey'] = res.pubkey; 12 | localStorage['privkey'] = res.privkey; 13 | account = res; 14 | }); 15 | } 16 | 17 | store.setAccount(account); 18 | store.setUploadedFiles(); 19 | }; 20 | -------------------------------------------------------------------------------- /web/src/js/actions/upload.js: -------------------------------------------------------------------------------- 1 | import APIClient from '../clients/api'; 2 | import EthClient from '../clients/ethereum'; 3 | import ed25519 from 'supercop.js'; 4 | import { Base64 } from 'js-base64'; 5 | import ab2buf from 'arraybuffer-to-buffer'; 6 | import { sha3_256 } from 'js-sha3'; 7 | 8 | export default (file, store) => { 9 | let signature = null; 10 | let token = null; 11 | let hash = null; 12 | return new Promise((resolve, _) => { 13 | const reader = new FileReader(); 14 | reader.onload = e => { 15 | const pubkey = Buffer.from( 16 | Base64.atob(localStorage['pubkey']), 17 | 'latin1' 18 | ); 19 | const privkey = Buffer.from( 20 | Base64.atob(localStorage['privkey']), 21 | 'latin1' 22 | ); 23 | const contentBuf = ab2buf(reader.result); 24 | signature = ed25519.sign(contentBuf, pubkey, privkey); 25 | hash = sha3_256(contentBuf); 26 | console.log(hash); 27 | 28 | return resolve(contentBuf); 29 | }; 30 | reader.readAsArrayBuffer(file); 31 | }) 32 | .then(content => new APIClient().upload(content, signature)) 33 | .then(res => res.json()) 34 | .then(res => { 35 | token = res.token; 36 | return new EthClient().store(hash); 37 | }) 38 | .then(txHash => { 39 | console.log('stored:', txHash); 40 | store.addUploadedFile({ 41 | name: file.name, 42 | hash: hash, 43 | token: token, 44 | txHash: txHash, 45 | date: file.lastModifiedDate 46 | }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /web/src/js/clients/api.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | 3 | export default class { 4 | constructor() {} 5 | 6 | upload(content, signature) { 7 | const payload = { 8 | content: Base64.btoa(content.toString('latin1')), 9 | signature: Base64.btoa(signature.toString('latin1')), 10 | pubkey: localStorage['pubkey'] 11 | }; 12 | return fetch('http://localhost:8080/v1/files', { 13 | mode: 'cors', 14 | method: 'POST', 15 | body: JSON.stringify(payload) 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/src/js/clients/ethereum.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | 3 | import Cris from '../../../contracts/Cris.sol'; 4 | import ed25519 from 'supercop.js'; 5 | import generatePassword from 'password-generator'; 6 | import { Base64 } from 'js-base64'; 7 | 8 | export default class { 9 | constructor() { 10 | console.log('init eth client'); 11 | const provider = new Web3.providers.HttpProvider( 12 | 'http://localhost:8545' 13 | ); 14 | 15 | Cris.setProvider(provider); 16 | this.cris = Cris.deployed(); 17 | this.web3 = new Web3(provider); 18 | } 19 | 20 | createAccount() { 21 | return new Promise((resolve, reject) => { 22 | console.log('create new account'); 23 | const pair = ed25519.createKeyPair(ed25519.createSeed()); 24 | 25 | const password = generatePassword(32); 26 | localStorage['password'] = password; 27 | 28 | this.web3.personal.newAccount(password, (err, address) => { 29 | if (err) { 30 | reject(err); 31 | } 32 | 33 | resolve({ 34 | address: address, 35 | pubkey: Base64.btoa(pair.publicKey.toString('latin1')), 36 | privkey: Base64.btoa(pair.secretKey.toString('latin1')) 37 | }); 38 | }); 39 | }); 40 | } 41 | 42 | unlock() { 43 | console.log(this.web3); 44 | this.web3.personal.unlockAccount( 45 | localStorage['address'], 46 | localStorage['password'], 47 | 24 * 60 * 60, 48 | err => { 49 | if (!err) { 50 | console.log(err); 51 | } 52 | console.log('unlocked'); 53 | } 54 | ); 55 | } 56 | 57 | store(hash) { 58 | this.unlock(); 59 | return this.cris.store(hash, { from: localStorage['address'] }); 60 | } 61 | 62 | fetch(_, hash) { 63 | console.log('fetch:', hash); 64 | this.cris.has.call(hash).then(res => { 65 | console.log('has: ', res); 66 | }); 67 | return this.cris.has.call(hash); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/src/js/index.js: -------------------------------------------------------------------------------- 1 | import 'riot'; 2 | 3 | import '../tags/app.tag'; 4 | import '../tags/upload.tag'; 5 | import '../tags/status.tag'; 6 | 7 | import 'riot-mui'; 8 | 9 | import 'clipboard'; 10 | 11 | import Store from './stores/store'; 12 | import APIClient from './clients/api'; 13 | import EthClient from './clients/ethereum'; 14 | import share from './share'; 15 | 16 | (() => { 17 | const parseQuery = queryString => { 18 | if (queryString.length == 0) { 19 | return {}; 20 | } 21 | 22 | let queries = {}; 23 | queryString 24 | .slice(1) 25 | .split('&') 26 | .forEach(pair => { 27 | const s = pair.split('='); 28 | queries[s[0]] = s.length == 2 ? s[1] : ''; 29 | }); 30 | 31 | return queries; 32 | }; 33 | 34 | const query = parseQuery(document.location.search); 35 | if (query['url']) { 36 | share(new EthClient(), query['url']); 37 | return; 38 | } 39 | 40 | riot.mount('*', { 41 | store: new Store(), 42 | apiClient: new APIClient(), 43 | ethClient: new EthClient() 44 | }); 45 | })(); 46 | -------------------------------------------------------------------------------- /web/src/js/share.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | 3 | const parseURL = url => { 4 | return Base64.decode(url).split('.'); 5 | }; 6 | const fetchContent = url => { 7 | const headers = new Headers(); 8 | headers.append( 9 | 'Accept', 10 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' 11 | ); 12 | return fetch(url, { 13 | mode: 'cors', 14 | headers: headers 15 | }).then(res => res.blob()); 16 | }; 17 | 18 | const verifyFileHash = (ethClient, hash) => { 19 | return ethClient.fetch('', hash); 20 | }; 21 | 22 | const doDownload = content => { 23 | const url = URL.createObjectURL(content); 24 | const body = document.querySelector('body'); 25 | const e = document.createElement('a'); 26 | e.setAttribute('href', url); 27 | e.setAttribute('download', ''); 28 | body.appendChild(e); 29 | e.click(); 30 | }; 31 | 32 | export default (ethClient, url) => { 33 | const [token, hash, privkey] = parseURL(url); 34 | 35 | const actualURL = `http://localhost:8080/v1/files/${token}`; 36 | 37 | verifyFileHash(ethClient, hash) 38 | .then(verified => { 39 | if (!verified) { 40 | throw Error('tempering detected'); 41 | } 42 | }) 43 | .then(() => fetchContent(actualURL)) 44 | .then(doDownload) 45 | .catch(e => { 46 | console.log(e); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /web/src/js/stores/store.js: -------------------------------------------------------------------------------- 1 | import 'riot'; 2 | 3 | export default class { 4 | constructor() { 5 | riot.observable(this); 6 | } 7 | 8 | saveAccount(pubkey) {} 9 | 10 | setUploadedFiles() { 11 | console.log('set uploaded'); 12 | this.uploaded = JSON.parse(localStorage['uploaded'] || '[]'); 13 | this.trigger('set_uploaded_files', this.uploaded); 14 | } 15 | 16 | setAccount(account) { 17 | this.account = account; 18 | this.trigger('get_account', account); 19 | } 20 | 21 | addUploadedFile(file) { 22 | this.uploaded.push(file); 23 | this.save(); 24 | 25 | this.trigger('add_uploaded_file', file); 26 | } 27 | 28 | save() { 29 | localStorage['uploaded'] = JSON.stringify(this.uploaded); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/src/tags/app.tag: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |

ETH : { address }

8 | 9 | 10 | 11 | 12 |
13 | 14 | 33 | 34 | 40 |
41 | -------------------------------------------------------------------------------- /web/src/tags/status.tag: -------------------------------------------------------------------------------- 1 | 2 |

Uploaded files:

3 | 4 | 5 | 9 | 10 | 11 |
6 | 7 | { name } 8 | { txHash.slice(0, 31) }
12 | 13 | 35 | 36 | 52 | 53 |
-------------------------------------------------------------------------------- /web/src/tags/upload.tag: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 10 |
11 | 12 |
️Submit
13 |
14 | 15 | 40 | 41 | 46 |
-------------------------------------------------------------------------------- /web/truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rpc: { 3 | host: 'localhost', 4 | // host: '35.196.45.234', 5 | port: '8545', 6 | network_id: '*' 7 | }, 8 | migrations_directory: './migrations' 9 | }; 10 | -------------------------------------------------------------------------------- /web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | // context: path.resolve(__dirname, "src"), 6 | entry: { 7 | app: './src/js/index.js' 8 | }, 9 | output: { 10 | path: __dirname + '/public/js', 11 | filename: 'app.js' 12 | }, 13 | plugins: [ 14 | new webpack.ProvidePlugin({ 15 | riot: 'riot' 16 | }), 17 | new webpack.DefinePlugin({ 18 | API_SERVER: "'https://mirage.syfm.space'" 19 | }) 20 | ], 21 | node: { 22 | fs: 'empty' 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.sol/, 28 | loader: 'truffle-solidity-loader' 29 | }, 30 | { 31 | test: /\.tag$/, 32 | enforce: 'pre', 33 | loader: 'riot-tag-loader' 34 | }, 35 | { 36 | test: /\.css$/, 37 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] 38 | }, 39 | { 40 | test: /\.js$|\.tag$/, 41 | enforce: 'post', 42 | loader: 'babel-loader', 43 | exclude: /node_modules/, 44 | query: { 45 | presets: ['es2015-riot'] 46 | } 47 | } 48 | ] 49 | }, 50 | devtool: 'inline-source-map', 51 | devServer: { 52 | compress: true, 53 | contentBase: 'public', 54 | publicPath: '/js', 55 | port: 3000, 56 | historyApiFallback: { 57 | disableDotRule: true 58 | } 59 | } 60 | }; 61 | --------------------------------------------------------------------------------