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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 876c9c726c38da76cf6cd5d55ce1dedac5890d91
4
- data.tar.gz: a523ca9e95aac6e104a6199f16d2ab2fd0ad41f2
3
+ metadata.gz: a59d62786a05ab1ca7d8c9b0c3b6bda41405a9aa
4
+ data.tar.gz: 69dfd3ecc8d0cf67afd0c0d1e2ef6395b16489df
5
5
  SHA512:
6
- metadata.gz: a57bfbe1a79fd96bc15bd19305041d606133f9ecc87bc252f52889edd7e2a74d95215fe0ca31e798c48e85108cffc2e1e450e177460213647edb2cfdd143bf28
7
- data.tar.gz: 3cb2dd80edba92b32365f26ca17156db02208c20bddf0a2bf0f9629466f52e7494ee03e90e27b4f45fac7da0885f959956ee0b9d2ed923dc1742a9ed6747f20c
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,6 @@
1
+ module StandardFile
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace StandardFile
4
+ engine_name 'standard_file'
5
+ end
6
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module StandardFile
2
+ VERSION = '0.1.3'
3
+ end
@@ -0,0 +1,8 @@
1
+ require "standard_file/engine"
2
+ require_relative 'standard_file/sync_manager'
3
+ require_relative 'standard_file/user_manager'
4
+ require_relative 'standard_file/jwt_helper'
5
+
6
+ module StandardFile
7
+
8
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :standard_file do
3
+ # # Task goes here
4
+ # 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.2
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-01-29 00:00:00.000000000 Z
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.6
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.6
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 as notes,
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