standard-file 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +37 -0
- data/lib/standard_file/engine.rb +6 -0
- data/lib/standard_file/jwt_helper.rb +17 -0
- data/lib/standard_file/sync_manager.rb +162 -0
- data/lib/standard_file/user_manager.rb +70 -0
- data/lib/standard_file/version.rb +3 -0
- data/lib/standard_file.rb +8 -0
- data/lib/tasks/standard_file_tasks.rake +4 -0
- metadata +17 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a59d62786a05ab1ca7d8c9b0c3b6bda41405a9aa
|
4
|
+
data.tar.gz: 69dfd3ecc8d0cf67afd0c0d1e2ef6395b16489df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b3738a096e0f49552c0f1afea404bc849d96d2141eaf2aebc5db7b6c15d59508c7e9b6bbf82b7d7259b8f5e9b1622b5da5275cadadab980804fdb559968e4b4c
|
7
|
+
data.tar.gz: c585d6b12c4b92cb02ed14a08ded3b15762b9f39e96e8299009036aa0e27357f5bc132f4658d488a30240a7beda3518af71a582a51f090a5750d9960d88ee92b
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# StandardFile
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'standard_file'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install standard_file
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'StandardFile'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
require 'bundler/gem_tasks'
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
task default: :test
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module StandardFile
|
2
|
+
module JwtHelper
|
3
|
+
require "jwt"
|
4
|
+
|
5
|
+
def self.encode(payload)
|
6
|
+
JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256')
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.decode(token)
|
10
|
+
return HashWithIndifferentAccess.new(JWT.decode(token, Rails.application.secrets.secret_key_base, true, { :algorithm => 'HS256' })[0])
|
11
|
+
rescue => exception
|
12
|
+
puts exception
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module StandardFile
|
2
|
+
class SyncManager
|
3
|
+
|
4
|
+
attr_accessor :sync_fields
|
5
|
+
|
6
|
+
def initialize(user)
|
7
|
+
@user = user
|
8
|
+
raise "User must be set" unless @user
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_sync_fields(val)
|
12
|
+
@sync_fields = val
|
13
|
+
end
|
14
|
+
|
15
|
+
def sync_fields
|
16
|
+
return @sync_fields || [:content, :enc_item_key, :content_type, :auth_hash, :deleted, :created_at]
|
17
|
+
end
|
18
|
+
|
19
|
+
def sync(item_hashes, options)
|
20
|
+
in_sync_token = options[:sync_token]
|
21
|
+
in_cursor_token = options[:cursor_token]
|
22
|
+
limit = options[:limit]
|
23
|
+
|
24
|
+
retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit).to_a
|
25
|
+
last_updated = DateTime.now
|
26
|
+
saved_items, unsaved = _sync_save(item_hashes)
|
27
|
+
if saved_items.length > 0
|
28
|
+
last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
|
29
|
+
end
|
30
|
+
|
31
|
+
# manage conflicts
|
32
|
+
saved_ids = saved_items.map{|x| x.uuid }
|
33
|
+
retrieved_ids = retrieved_items.map{|x| x.uuid }
|
34
|
+
conflicts = saved_ids & retrieved_ids # & is the intersection
|
35
|
+
# saved items take precedence, retrieved items are duplicated with a new uuid
|
36
|
+
conflicts.each do |conflicted_uuid|
|
37
|
+
# if changes are greater than 60 seconds apart, create conflicted copy, otherwise discard conflicted
|
38
|
+
saved = saved_items.find{|i| i.uuid == conflicted_uuid}
|
39
|
+
conflicted = retrieved_items.find{|i| i.uuid == conflicted_uuid}
|
40
|
+
if (saved.updated_at - conflicted.updated_at).abs > 60
|
41
|
+
puts "\n\n\n Creating conflicted copy of #{saved.uuid}\n\n\n"
|
42
|
+
dup = conflicted.dup
|
43
|
+
dup.user = conflicted.user
|
44
|
+
dup.save
|
45
|
+
retrieved_items.push(dup)
|
46
|
+
end
|
47
|
+
retrieved_items.delete(conflicted)
|
48
|
+
end
|
49
|
+
|
50
|
+
sync_token = sync_token_from_datetime(last_updated)
|
51
|
+
return {
|
52
|
+
:retrieved_items => retrieved_items,
|
53
|
+
:saved_items => saved_items,
|
54
|
+
:unsaved => unsaved,
|
55
|
+
:sync_token => sync_token,
|
56
|
+
:cursor_token => cursor_token
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def destroy_items(uuids)
|
61
|
+
items = @user.items.where(uuid: uuids)
|
62
|
+
items.destroy_all
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def sync_token_from_datetime(datetime)
|
69
|
+
version = 1
|
70
|
+
Base64.encode64("#{version}:" + "#{datetime.to_i}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def datetime_from_sync_token(sync_token)
|
74
|
+
decoded = Base64.decode64(sync_token)
|
75
|
+
parts = decoded.rpartition(":")
|
76
|
+
timestamp_string = parts.last
|
77
|
+
date = DateTime.strptime(timestamp_string,'%s')
|
78
|
+
return date
|
79
|
+
end
|
80
|
+
|
81
|
+
def _sync_save(item_hashes)
|
82
|
+
if !item_hashes
|
83
|
+
return [], []
|
84
|
+
end
|
85
|
+
saved_items = []
|
86
|
+
unsaved = []
|
87
|
+
|
88
|
+
item_hashes.each do |item_hash|
|
89
|
+
begin
|
90
|
+
item = @user.items.find_or_create_by(:uuid => item_hash[:uuid])
|
91
|
+
rescue => error
|
92
|
+
unsaved.push({
|
93
|
+
:item => item_hash,
|
94
|
+
:error => {:message => error.message, :tag => "uuid_conflict"}
|
95
|
+
})
|
96
|
+
next
|
97
|
+
end
|
98
|
+
|
99
|
+
item.update(item_hash.permit(*permitted_params))
|
100
|
+
# we want to force update the updated_at field, even if no changes were made
|
101
|
+
# item.touch
|
102
|
+
|
103
|
+
if item.deleted == true
|
104
|
+
set_deleted(item)
|
105
|
+
item.save
|
106
|
+
end
|
107
|
+
|
108
|
+
saved_items.push(item)
|
109
|
+
end
|
110
|
+
|
111
|
+
return saved_items, unsaved
|
112
|
+
end
|
113
|
+
|
114
|
+
def _sync_get(sync_token, input_cursor_token, limit)
|
115
|
+
cursor_token = nil
|
116
|
+
if limit == nil
|
117
|
+
limit = 100000
|
118
|
+
end
|
119
|
+
|
120
|
+
# if both are present, cursor_token takes precendence as that would eventually return all results
|
121
|
+
# the distinction between getting results for a cursor and a sync token is that cursor results use a
|
122
|
+
# >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
|
123
|
+
# typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
|
124
|
+
# by using >=, we don't miss those results on a subsequent call with a cursor token
|
125
|
+
if input_cursor_token
|
126
|
+
date = datetime_from_sync_token(input_cursor_token)
|
127
|
+
items = @user.items.order(:updated_at).where("updated_at >= ?", date)
|
128
|
+
elsif sync_token
|
129
|
+
date = datetime_from_sync_token(sync_token)
|
130
|
+
items = @user.items.order(:updated_at).where("updated_at > ?", date)
|
131
|
+
else
|
132
|
+
items = @user.items.order(:updated_at)
|
133
|
+
end
|
134
|
+
|
135
|
+
items = items.sort_by{|m| m.updated_at}
|
136
|
+
|
137
|
+
if items.count > limit
|
138
|
+
items = items.slice(0, limit)
|
139
|
+
date = items.last.updated_at
|
140
|
+
cursor_token = sync_token_from_datetime(date)
|
141
|
+
end
|
142
|
+
|
143
|
+
return items, cursor_token
|
144
|
+
end
|
145
|
+
|
146
|
+
def set_deleted(item)
|
147
|
+
item.deleted = true
|
148
|
+
item.content = nil if item.has_attribute?(:content)
|
149
|
+
item.enc_item_key = nil if item.has_attribute?(:enc_item_key)
|
150
|
+
item.auth_hash = nil if item.has_attribute?(:auth_hash)
|
151
|
+
end
|
152
|
+
|
153
|
+
def item_params
|
154
|
+
params.permit(*permitted_params)
|
155
|
+
end
|
156
|
+
|
157
|
+
def permitted_params
|
158
|
+
sync_fields
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module StandardFile
|
2
|
+
class UserManager
|
3
|
+
|
4
|
+
def initialize(user_class, salt_psuedo_nonce)
|
5
|
+
@user_class = user_class
|
6
|
+
@salt_psuedo_nonce = salt_psuedo_nonce
|
7
|
+
end
|
8
|
+
|
9
|
+
def sign_in(email, password)
|
10
|
+
user = @user_class.find_by_email(email)
|
11
|
+
if user and test_password(password, user.encrypted_password)
|
12
|
+
return { user: user, token: jwt(user) }
|
13
|
+
else
|
14
|
+
return {:error => {:message => "Invalid email or password.", :status => 401}}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def register(email, password, params)
|
19
|
+
user = @user_class.find_by_email(email)
|
20
|
+
if user
|
21
|
+
return {:error => {:message => "Unable to register.", :status => 401}}
|
22
|
+
else
|
23
|
+
user = @user_class.new(:email => email, :encrypted_password => hash_password(password))
|
24
|
+
user.update!(registration_params(params))
|
25
|
+
return { user: user, token: jwt(user) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def change_pw(user, password, params)
|
30
|
+
user.encrypted_password = hash_password(password)
|
31
|
+
user.update!(registration_params(params))
|
32
|
+
return { user: user, token: jwt(user) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def auth_params(email)
|
36
|
+
user = @user_class.find_by_email(email)
|
37
|
+
pw_salt = user ? Digest::SHA1.hexdigest(email + "SN" + user.pw_nonce) : Digest::SHA1.hexdigest(email + "SN" + @salt_psuedo_nonce)
|
38
|
+
pw_cost = user ? user.pw_cost : 5000
|
39
|
+
pw_alg = user ? user.pw_alg : "sha512"
|
40
|
+
pw_key_size = user ? user.pw_key_size : 512
|
41
|
+
pw_func = user ? user.pw_func : "pbkdf2"
|
42
|
+
return {:pw_func => pw_func, :pw_alg => pw_alg, :pw_salt => pw_salt, :pw_cost => pw_cost, :pw_key_size => pw_key_size}
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
require "bcrypt"
|
48
|
+
|
49
|
+
DEFAULT_COST = 11
|
50
|
+
|
51
|
+
def hash_password(password)
|
52
|
+
BCrypt::Password.create(password, cost: DEFAULT_COST).to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_password(password, hash)
|
56
|
+
bcrypt = BCrypt::Password.new(hash)
|
57
|
+
password = BCrypt::Engine.hash_secret(password, bcrypt.salt)
|
58
|
+
return password == hash
|
59
|
+
end
|
60
|
+
|
61
|
+
def jwt(user)
|
62
|
+
JwtHelper.encode({:user_uuid => user.uuid, :pw_hash => Digest::SHA256.hexdigest(user.encrypted_password)})
|
63
|
+
end
|
64
|
+
|
65
|
+
def registration_params(params)
|
66
|
+
params.permit(:pw_func, :pw_alg, :pw_cost, :pw_key_size, :pw_nonce)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: standard-file
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Standard File
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.5.
|
33
|
+
version: 1.5.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.5.
|
40
|
+
version: 1.5.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bcrypt
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,14 +52,24 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.1'
|
55
|
-
description: Standard File allows for storage, sync, encryption of items such
|
56
|
-
tags, and any other custom schema.
|
55
|
+
description: Standard File allows for storage, sync, and encryption of items such
|
56
|
+
as notes, tags, and any other models with a custom schema.
|
57
57
|
email:
|
58
58
|
- me@bitar.io
|
59
59
|
executables: []
|
60
60
|
extensions: []
|
61
61
|
extra_rdoc_files: []
|
62
|
-
files:
|
62
|
+
files:
|
63
|
+
- MIT-LICENSE
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- lib/standard_file.rb
|
67
|
+
- lib/standard_file/engine.rb
|
68
|
+
- lib/standard_file/jwt_helper.rb
|
69
|
+
- lib/standard_file/sync_manager.rb
|
70
|
+
- lib/standard_file/user_manager.rb
|
71
|
+
- lib/standard_file/version.rb
|
72
|
+
- lib/tasks/standard_file_tasks.rake
|
63
73
|
homepage: https://standardnotes.org
|
64
74
|
licenses:
|
65
75
|
- GPLv3
|