sinatra_sockets 0.0.6 → 0.0.7
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/README.md +191 -17
- data/lib/server_skeleton/Gemfile +9 -0
- data/lib/server_skeleton/Gemfile.lock +18 -0
- data/lib/server_skeleton/Procfile +2 -0
- data/lib/server_skeleton/README.md +1 -24
- data/lib/server_skeleton/Rakefile +9 -0
- data/lib/server_skeleton/crud_generator.rb +191 -0
- data/lib/server_skeleton/db/migrate/20170312215739_create_todos.rb +8 -0
- data/lib/server_skeleton/db/schema.rb +21 -0
- data/lib/server_skeleton/models.rb +14 -0
- data/lib/server_skeleton/server.rb +84 -86
- data/lib/server_skeleton/server_push.rb +53 -0
- data/lib/server_skeleton/{lib/routes/ws.rb → ws.rb} +12 -11
- data/lib/version.rb +1 -1
- metadata +9 -3
- data/lib/server_skeleton/lib/routes.rb +0 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b8e1fe4fafda9f7d0379477ab71a09f9d5c3527
|
4
|
+
data.tar.gz: 54a509e27709689f86e114ad309f96acffafa3f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3194158b45622e04b8e988cf89f49bf72549c1db9aa32ce47a1d0e6fb7eda730d38430ab87e2e7fc927176353f9fcd7a24c0e91b77b0f6c07611e9850d100b0d
|
7
|
+
data.tar.gz: 378b5fcf85f0cf8285bffac8164c596166c51bd0f0ce99d2ef3750f40d2a2998856ac91e7a3bfc0360e047a895eea1eb75726673f9a788bc191e560e047e9f19
|
data/README.md
CHANGED
@@ -1,3 +1,32 @@
|
|
1
|
+
## Sinatra sockets
|
2
|
+
|
3
|
+
### about
|
4
|
+
|
5
|
+
A boilerplate including:
|
6
|
+
|
7
|
+
- Sinatra
|
8
|
+
- Github oAuth
|
9
|
+
- Faye websockets, with token (not session) based auth so the front-end can be
|
10
|
+
on a different host.
|
11
|
+
- ActiveRecord
|
12
|
+
- CRUD generator for routes
|
13
|
+
- Server-push module to alert clients of db updates.
|
14
|
+
|
15
|
+
Note that at this point this project is the _exact same_ as the server component
|
16
|
+
of [vue-sinatra-boiler](http://github.com/maxpleaner/vue-sinatra-boiler).
|
17
|
+
Originally this included a minimal front-end, but upstream development ended up
|
18
|
+
happening on an API-only variant, and that was the most full-featured version.
|
19
|
+
|
20
|
+
In the future I would like to add generator options for components that are not
|
21
|
+
necessarily dependent on one another, such as:
|
22
|
+
|
23
|
+
- whether or not to include simple (no auth) front end demo page for websockets
|
24
|
+
- whether or not to include Activerecord with crud generator and server-push module
|
25
|
+
- Whether or not to add oAuth with token-based websocket credentials
|
26
|
+
|
27
|
+
### installing
|
28
|
+
|
29
|
+
|
1
30
|
```sh
|
2
31
|
$ gem install sinatra_sockets
|
3
32
|
$ sinatra_sockets generate .
|
@@ -6,34 +35,179 @@ $ sinatra_sockets generate .
|
|
6
35
|
It will print the generated directory tree:
|
7
36
|
|
8
37
|
```txt
|
9
|
-
|
10
|
-
./server_skeleton
|
38
|
+
.
|
11
39
|
├── config.ru
|
40
|
+
├── crud_generator.rb
|
41
|
+
├── db
|
42
|
+
│ ├── migrate
|
43
|
+
│ │ └── 20170312215739_create_todos.rb
|
44
|
+
│ └── schema.rb
|
12
45
|
├── Gemfile
|
13
46
|
├── Gemfile.lock
|
14
|
-
├──
|
15
|
-
|
16
|
-
|
17
|
-
│ └── routes.rb
|
47
|
+
├── models.rb
|
48
|
+
├── Procfile
|
49
|
+
├── Rakefile
|
18
50
|
├── README.md
|
19
|
-
|
51
|
+
├── server_push.rb
|
52
|
+
├── server.rb
|
53
|
+
└── ws.rb
|
20
54
|
|
21
|
-
2 directories,
|
55
|
+
2 directories, 13 files
|
22
56
|
```
|
23
57
|
|
24
|
-
|
58
|
+
Go to Github settings and create an oAuth application
|
59
|
+
|
60
|
+
Set up the server:
|
25
61
|
|
26
62
|
```sh
|
27
63
|
cd server_skeleton
|
28
|
-
bundle
|
29
|
-
|
64
|
+
bundle install
|
65
|
+
bundle exec rake db:create db:migrate
|
66
|
+
cp .env.example .env
|
67
|
+
nano .env # add the github credentials here
|
68
|
+
```
|
69
|
+
|
70
|
+
Start the server with thin (port defaults to 3000):
|
71
|
+
|
72
|
+
```sh
|
73
|
+
bundle exec thin start -p 3000
|
30
74
|
```
|
31
75
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
76
|
+
### components in detail:
|
77
|
+
|
78
|
+
**database / models**
|
79
|
+
|
80
|
+
this uses `sinatra-activerecord` and is similar to what's found in rails.
|
81
|
+
|
82
|
+
To generate a migration: `bundle exec rake db:create_migration NAME=my_migration_name`
|
83
|
+
|
84
|
+
Then customize it and `bundle exec rake db:migrate`.
|
85
|
+
|
86
|
+
`server.rb` contains the database configuration inline in the ruby code.
|
87
|
+
|
88
|
+
Models are all listed in `models.rb`, but it isn't necessary to do so. There are
|
89
|
+
no dynamic requires happening in this application - everything is individually
|
90
|
+
required in `server.rb`.
|
91
|
+
|
92
|
+
If using the crud generator or server-push, there is one required method that all
|
93
|
+
models must respond to: `public_attributes`. It returns a hash which is a filtered
|
94
|
+
version of `attributes` (suitable to be sent to clients).
|
95
|
+
|
96
|
+
**auth**
|
97
|
+
|
98
|
+
Here's how the auth flow works (the pieces are in `server.rb` and `ws.rb`):
|
99
|
+
|
100
|
+
-
|
101
|
+
- Client checks their cookies to see if there's a token.
|
102
|
+
- If there's not, one is requested from the server and saved as a cookie.
|
103
|
+
- Client establishes a websocket connection with server, passing token in query params
|
104
|
+
-
|
105
|
+
- If the client found the token in the query, it will send a websocket request to try and authenticate.
|
106
|
+
- If the server had stored credentials for that token, the client get a success response over websockets.
|
107
|
+
- otherwise the client will get no response
|
108
|
+
- If the client had to fetch a new token, the authentication is put on hold until the user clicks the
|
109
|
+
"authenticate with github" button, passing the token to the server
|
110
|
+
- The server makes the client through the Github auth flow but stores the token ref.
|
111
|
+
- When Github auth is done clients get a ws message (looked up by token) saying they're authenticated.
|
112
|
+
|
113
|
+
|
114
|
+
**crud generator**
|
115
|
+
|
116
|
+
In the Sinatra server definition in `server.rb`, it's included with
|
117
|
+
`register Sinatra::CrudGenerator`. This exposes one behemoth of a method:
|
118
|
+
`crud_generate`.
|
119
|
+
|
120
|
+
Here's its annotated signature (all the options are keyword args and are optional unless stated)
|
121
|
+
|
122
|
+
```rb
|
123
|
+
def crud_generate(
|
124
|
+
|
125
|
+
# String such as "todo" (required).
|
126
|
+
# The singular and pluralized forms are used to define routes i.e. /todos or /todo
|
127
|
+
resource:,
|
128
|
+
|
129
|
+
# Class such as Todo (required)
|
130
|
+
resource_class:,
|
131
|
+
|
132
|
+
# String such as '/api/', prefixed on all routes (defaults to '/')
|
133
|
+
root_path: '/',
|
134
|
+
|
135
|
+
# Hash. If this is defined, the `cross_origin` method is invoked with it as an arument.
|
136
|
+
# e.g. { cross_origin: "http://localhost:8080" }
|
137
|
+
# would allow all these CRUD routes to be hit by this origin.
|
138
|
+
cross_origin_opts: nil,
|
139
|
+
|
140
|
+
# A Proc which is passed the request and returns something truthy if the auth failed.
|
141
|
+
# For example if it returns { error: "bad reqeust" }.to_json that will be the
|
142
|
+
# final return value of the route and it will return early.
|
143
|
+
# If provided, this proc will apply to all of the routes, though each route can have
|
144
|
+
# it's own proc which will override it (see below)
|
145
|
+
auth: nil,
|
146
|
+
|
147
|
+
# Each route has its own option which is a hash for configuration.
|
148
|
+
# These are optional and defaults are set (see crud_generator.rb).
|
149
|
+
# There are slight differences in the configuration hashes accepted per route,
|
150
|
+
# but here is the full list of keys (all are optional):
|
151
|
+
#
|
152
|
+
# method: Symbol (i.e. :get or :post, must be supported by Sinatra)
|
153
|
+
#
|
154
|
+
# path: String (defaults to "/#{resource}" or "/#{plural_resource}"
|
155
|
+
#
|
156
|
+
# auth: Proc with the same signature as the one discussed before
|
157
|
+
#
|
158
|
+
# filter: Proc which is used on index route. Is passed an ActiveRecord Query of all records
|
159
|
+
# of the record_class and returns some subset which gets sent to the client.
|
160
|
+
#
|
161
|
+
# secure_params: Proc used on create/update which is passed the request object and returns
|
162
|
+
# a list of the accepted param keys (params are filtered to only include these)
|
163
|
+
#
|
164
|
+
|
165
|
+
# Hash with :method, :path, :auth, and :filter keys
|
166
|
+
index: nil,
|
167
|
+
|
168
|
+
# Hash with :method, :path, :auth, and :secure_params keys
|
169
|
+
create: nil,
|
170
|
+
|
171
|
+
# Hash with :method, :path, and :auth keys
|
172
|
+
read: nil,
|
173
|
+
|
174
|
+
# hash with :method, :path, :auth, and :secure_params keys
|
175
|
+
update: nil,
|
176
|
+
|
177
|
+
# hash with :method, :path, and :auth keys
|
178
|
+
destroy: nil,
|
179
|
+
|
180
|
+
# Array of symbols such as [:create, :update], will skip generating these routes.
|
181
|
+
except: []
|
182
|
+
)
|
183
|
+
```
|
184
|
+
|
185
|
+
**Server push**
|
186
|
+
|
187
|
+
This is a module which can be `included` into any model.
|
188
|
+
|
189
|
+
It patches three foundational methods in ActiveRecord:
|
190
|
+
|
191
|
+
- `save` handles the case of creating new records.
|
192
|
+
- although this method is called under the hood by both `create` and `update`,
|
193
|
+
only the `create` case is handled here because there's a check whether the record is
|
194
|
+
persisted and previously unsaved.
|
195
|
+
- `update` - handles when existing records are changed
|
196
|
+
- `destroy` - handles when records are deleted.
|
197
|
+
|
198
|
+
So after `include ServerPush` is placed in the model, any calls to `create`
|
199
|
+
(or `save`) on an unsaved record will trigger a "add_record" ws message to be sent.
|
200
|
+
Any call to `destroy` will trigger "destroy_record", and similarly `update` triggers
|
201
|
+
"update_record"
|
202
|
+
|
203
|
+
By default all sockets get these messages sent to them, though this can be configured
|
204
|
+
by defining a `publish_to` method in the model that returns a list of sockets.
|
205
|
+
|
206
|
+
All of the outgoing websocket messages in server-push are hashes with this signature:
|
36
207
|
|
37
|
-
|
208
|
+
- action: string ("add_record", "update_record", or "destroy_record")
|
209
|
+
- type: string i.e. "todo"
|
210
|
+
- record: Hash with the model's `public_attributes`.
|
38
211
|
|
39
|
-
|
212
|
+
[vue-sinatra-boiler](http://github.com/maxpleaner/vue-sinatra-boiler) has the front-end
|
213
|
+
API in place to use this.
|
data/lib/server_skeleton/Gemfile
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
GEM
|
2
2
|
remote: http://rubygems.org/
|
3
3
|
specs:
|
4
|
+
activemodel (5.0.1)
|
5
|
+
activesupport (= 5.0.1)
|
6
|
+
activerecord (5.0.1)
|
7
|
+
activemodel (= 5.0.1)
|
8
|
+
activesupport (= 5.0.1)
|
9
|
+
arel (~> 7.0)
|
4
10
|
activesupport (5.0.1)
|
5
11
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
6
12
|
i18n (~> 0.7)
|
@@ -8,6 +14,7 @@ GEM
|
|
8
14
|
tzinfo (~> 1.1)
|
9
15
|
addressable (2.5.0)
|
10
16
|
public_suffix (~> 2.0, >= 2.0.2)
|
17
|
+
arel (7.1.4)
|
11
18
|
byebug (9.0.6)
|
12
19
|
concurrent-ruby (1.0.4)
|
13
20
|
daemons (1.2.4)
|
@@ -23,12 +30,14 @@ GEM
|
|
23
30
|
multipart-post (2.0.0)
|
24
31
|
octokit (4.6.2)
|
25
32
|
sawyer (~> 0.8.0, >= 0.5.3)
|
33
|
+
pg (0.19.0)
|
26
34
|
public_suffix (2.0.5)
|
27
35
|
rack (1.6.5)
|
28
36
|
rack-protection (1.5.3)
|
29
37
|
rack
|
30
38
|
rack-proxy (0.6.0)
|
31
39
|
rack
|
40
|
+
rake (12.0.0)
|
32
41
|
sawyer (0.8.1)
|
33
42
|
addressable (>= 2.3.5, < 2.6)
|
34
43
|
faraday (~> 0.8, < 1.0)
|
@@ -36,10 +45,14 @@ GEM
|
|
36
45
|
rack (~> 1.5)
|
37
46
|
rack-protection (~> 1.4)
|
38
47
|
tilt (>= 1.3, < 3)
|
48
|
+
sinatra-activerecord (2.0.11)
|
49
|
+
activerecord (>= 3.2)
|
50
|
+
sinatra (~> 1.0)
|
39
51
|
sinatra-cross_origin (0.4.0)
|
40
52
|
sinatra_auth_github (1.2.0)
|
41
53
|
sinatra (~> 1.0)
|
42
54
|
warden-github (~> 1.2.0)
|
55
|
+
sqlite3 (1.3.10)
|
43
56
|
thin (1.7.0)
|
44
57
|
daemons (~> 1.0, >= 1.0.9)
|
45
58
|
eventmachine (~> 1.0, >= 1.0.4)
|
@@ -62,13 +75,18 @@ PLATFORMS
|
|
62
75
|
ruby
|
63
76
|
|
64
77
|
DEPENDENCIES
|
78
|
+
activesupport
|
65
79
|
byebug
|
66
80
|
dotenv
|
67
81
|
faye-websocket
|
82
|
+
pg
|
68
83
|
rack-proxy
|
84
|
+
rake
|
69
85
|
sinatra
|
86
|
+
sinatra-activerecord
|
70
87
|
sinatra-cross_origin
|
71
88
|
sinatra_auth_github
|
89
|
+
sqlite3
|
72
90
|
thin
|
73
91
|
|
74
92
|
BUNDLED WITH
|
@@ -1,24 +1 @@
|
|
1
|
-
|
2
|
-
---
|
3
|
-
|
4
|
-
This server is adapted from
|
5
|
-
[sinatra_sockets](http://github.com/maxpleaner/sinatra_sockets),
|
6
|
-
another boiler I made.
|
7
|
-
|
8
|
-
---
|
9
|
-
|
10
|
-
Steps:
|
11
|
-
|
12
|
-
1. bundle
|
13
|
-
2. thin start
|
14
|
-
|
15
|
-
---
|
16
|
-
|
17
|
-
This version has a lot of stuff removed since it's serving no front-end.
|
18
|
-
It's just the API for the webpack client
|
19
|
-
|
20
|
-
Important files:
|
21
|
-
- websocket stuff in lib/routes/ws.rb
|
22
|
-
- regular routes in server.rb
|
23
|
-
|
24
|
-
---
|
1
|
+
See the readme at [sinatra_sockets](http://github.com/maxpleaner/sinatra_sockets)
|
@@ -0,0 +1,191 @@
|
|
1
|
+
#
|
2
|
+
# Defines the "crud_generate" method to create routes
|
3
|
+
# Load this using "register Sinatra::CrudGenerator"
|
4
|
+
#
|
5
|
+
# Note that if the :cross_origin_opts key in the crud_generate options is set,
|
6
|
+
# then sinatra-cross_origin is a dependency and "register Sinatra::CrossOrigin"
|
7
|
+
# should be run first
|
8
|
+
#
|
9
|
+
|
10
|
+
module Sinatra
|
11
|
+
|
12
|
+
module CrudGenerator
|
13
|
+
|
14
|
+
# private
|
15
|
+
def get_default_secure_params(resource_class)
|
16
|
+
Proc.new do |request|
|
17
|
+
resource_class.new.attributes.keys.reject do |key|
|
18
|
+
key.in? %w{id created_at updated_at}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# public
|
24
|
+
def crud_generate(
|
25
|
+
resource:, resource_class:, root_path: '/',
|
26
|
+
cross_origin_opts: nil, auth: nil,
|
27
|
+
index: nil, create: nil, read: nil, update: nil, destroy: nil,
|
28
|
+
except: []
|
29
|
+
)
|
30
|
+
|
31
|
+
# the auth argument is a proc that can be used to disallow some requests.
|
32
|
+
# It is passed the request as an argument.
|
33
|
+
# It it returns something truthy, then the result of the auth block is sent
|
34
|
+
# to the client and the route goes no further.
|
35
|
+
# That's why the default proc returns false (to allow all requests)
|
36
|
+
|
37
|
+
# The secure params (for update and create) defaults to all the keys except
|
38
|
+
# id, created_at, and updated_at
|
39
|
+
# It can be specified for a particular route
|
40
|
+
# e.g. to allow no params on create:
|
41
|
+
# crud_generate(
|
42
|
+
# ... other opts (resource and resource_class are mandatory)
|
43
|
+
# create: { secure_params: Proc.new { |request| [] } }
|
44
|
+
# )
|
45
|
+
|
46
|
+
if cross_origin_opts
|
47
|
+
# Allow CORS preflight requests.
|
48
|
+
# Each individual route still needs to state a CORS policy if it has one.
|
49
|
+
before do
|
50
|
+
if request.request_method == 'OPTIONS'
|
51
|
+
response.headers["Access-Control-Allow-Origin"] = CLIENT_BASE_URL
|
52
|
+
response.headers["Access-Control-Allow-Methods"] = "POST,DELETE,PUT,GET"
|
53
|
+
halt 200
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
plural = resource.pluralize
|
59
|
+
raise(
|
60
|
+
ArgumentError, "resource does not have a simple plural"
|
61
|
+
) unless plural.eql?(resource + "s")
|
62
|
+
|
63
|
+
default_secure_params_proc = get_default_secure_params(resource_class)
|
64
|
+
|
65
|
+
index ||= {}
|
66
|
+
index = {
|
67
|
+
method: :get,
|
68
|
+
path: "/#{plural}",
|
69
|
+
auth: auth || Proc.new { |request| false },
|
70
|
+
filter: Proc.new { |records| records }
|
71
|
+
}.merge(index)
|
72
|
+
|
73
|
+
create ||= {}
|
74
|
+
create = {
|
75
|
+
method: :post,
|
76
|
+
path: "/#{plural}",
|
77
|
+
auth: auth || Proc.new { |request| false },
|
78
|
+
secure_params: default_secure_params_proc
|
79
|
+
}.merge(create)
|
80
|
+
|
81
|
+
read ||= {}
|
82
|
+
read = {
|
83
|
+
method: :get,
|
84
|
+
path: "/#{resource}",
|
85
|
+
auth: auth || Proc.new { |request| false }
|
86
|
+
}.merge(read)
|
87
|
+
|
88
|
+
update ||= {}
|
89
|
+
update = {
|
90
|
+
method: :put,
|
91
|
+
path: "/#{resource}",
|
92
|
+
auth: auth || Proc.new { |request| false },
|
93
|
+
secure_params: default_secure_params_proc
|
94
|
+
}.merge(update)
|
95
|
+
|
96
|
+
destroy ||= {}
|
97
|
+
destroy = {
|
98
|
+
method: :delete,
|
99
|
+
path: "/#{resource}",
|
100
|
+
auth: auth || Proc.new { |request| false }
|
101
|
+
}.merge(destroy)
|
102
|
+
|
103
|
+
unless except.include?(:index)
|
104
|
+
send(index[:method], index[:path]) do
|
105
|
+
cross_origin(cross_origin_opts) if cross_origin_opts
|
106
|
+
auth_result = index[:auth].call(request)
|
107
|
+
return auth_result if auth_result
|
108
|
+
{
|
109
|
+
success: index[:filter].call(resource_class.all).map(&:public_attributes)
|
110
|
+
}.to_json
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Calls ActiveRecord "save" (via "create"), which is patched by ServerPush
|
115
|
+
unless except.include?(:create)
|
116
|
+
send(create[:method], create[:path]) do
|
117
|
+
cross_origin(cross_origin_opts) if cross_origin_opts
|
118
|
+
auth_result = create[:auth].call(request)
|
119
|
+
return auth_result if auth_result
|
120
|
+
filtered_params = params.select do |key, val|
|
121
|
+
key.in? *create[:secure_params].call(request)
|
122
|
+
end
|
123
|
+
created = resource_class.create(filtered_params)
|
124
|
+
if created.persisted?
|
125
|
+
{ success: created.public_attributes }.to_json
|
126
|
+
else
|
127
|
+
{ error: created.errors.full_messages }.to_json
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
unless except.include?(:read)
|
133
|
+
send(read[:method], read[:path]) do
|
134
|
+
cross_origin(cross_origin_opts) if cross_origin_opts
|
135
|
+
auth_result = read[:auth].call(request)
|
136
|
+
return auth_result if auth_result
|
137
|
+
found = resource_class.find_by(id: params[:id])
|
138
|
+
if found
|
139
|
+
{ success: found.attributes }.to_json
|
140
|
+
else
|
141
|
+
{ error: ["not found"] }.to_json
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# calls ActiveRecord "update", which is patched by ServerPush
|
147
|
+
unless except.include?(:update)
|
148
|
+
send(update[:method], update[:path]) do
|
149
|
+
cross_origin(cross_origin_opts) if cross_origin_opts
|
150
|
+
auth_result = update[:auth].call(request)
|
151
|
+
return auth_result if auth_result
|
152
|
+
filtered_params = params.select do |key, val|
|
153
|
+
key.in? *create[:secure_params].call(request)
|
154
|
+
end
|
155
|
+
found = resource_class.find_by(id: params[:id])
|
156
|
+
if found
|
157
|
+
update[:secure_params].call.each do |key|
|
158
|
+
found.send(:"#{key}=", params[key])
|
159
|
+
end
|
160
|
+
if found.valid?
|
161
|
+
found.update({})
|
162
|
+
{ success: found.public_attributes }.to_json
|
163
|
+
else
|
164
|
+
{ error: found.errors.full_messages }.to_json
|
165
|
+
end
|
166
|
+
else
|
167
|
+
{ error: ["not found"] }.to_json
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# calls ActiveRecord "destroy" which is patched by ServerPush
|
173
|
+
unless except.include?(:destroy)
|
174
|
+
send(destroy[:method], destroy[:path]) do
|
175
|
+
cross_origin(cross_origin_opts) if cross_origin_opts
|
176
|
+
auth_result = destroy[:auth].call(request)
|
177
|
+
return auth_result if auth_result
|
178
|
+
found = resource_class.find_by(id: params[:id])
|
179
|
+
if found
|
180
|
+
found.destroy!
|
181
|
+
{ success: found.attributes }.to_json
|
182
|
+
else
|
183
|
+
{ error: ["not found"] }.to_json
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end # /crud_generate
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# Note that this schema.rb definition is the authoritative source for your
|
6
|
+
# database schema. If you need to create the application database on another
|
7
|
+
# system, you should be using db:schema:load, not running all the migrations
|
8
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
9
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(version: 20170312215739) do
|
14
|
+
|
15
|
+
create_table "todos", force: :cascade do |t|
|
16
|
+
t.string "text"
|
17
|
+
t.datetime "created_at", null: false
|
18
|
+
t.datetime "updated_at", null: false
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -1,71 +1,54 @@
|
|
1
|
-
# ================================================
|
2
|
-
# Entry to the Sinatra server
|
3
|
-
# ------------------------------------------------
|
4
|
-
# This file should not be run directly
|
5
|
-
# Run it with "thin start"
|
6
|
-
# ================================================
|
7
|
-
|
8
1
|
require 'sinatra/base'
|
2
|
+
require "sinatra/activerecord"
|
9
3
|
require 'faye/websocket'
|
10
4
|
require 'byebug'
|
11
5
|
require 'sinatra_auth_github'
|
12
6
|
require('dotenv'); Dotenv.load
|
13
7
|
require 'sinatra/cross_origin'
|
8
|
+
require 'active_support/all'
|
14
9
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# files according to their contained class hierarchies.
|
20
|
-
# i.e. class Foo would be in foo.rb,
|
21
|
-
# class Foo::Bar would be in foo/bar.rb,
|
22
|
-
#
|
23
|
-
# If there is the situation where a class depends on another that is in a
|
24
|
-
# deeper-nested file, there's always the option to pass the dependency at
|
25
|
-
# runtime.
|
26
|
-
|
27
|
-
Dir.glob("./**/*.rb").sort_by { |x| x.count("/") }.each do |path|
|
28
|
-
require path
|
29
|
-
end
|
10
|
+
require './crud_generator'
|
11
|
+
require './server_push'
|
12
|
+
require './models'
|
13
|
+
require './ws'
|
30
14
|
|
31
|
-
# The REST routes (listed in this file) cannot store information in the session
|
32
|
-
# since it's on another host.
|
33
|
-
# Rather, they pass back and forth a token identifier
|
34
|
-
#
|
35
|
-
# This is the :token param, and is required on all routes except for
|
36
|
-
# /token
|
37
|
-
#
|
38
|
-
# Here's an outline of the flow:
|
39
|
-
#
|
40
|
-
# 1. Client hits GET /token, gets a new token
|
41
|
-
# 2. Client sends token with websocket connection request at GET /ws
|
42
|
-
# 3. Client hits GET /authenticate and goes through Github oAuth login
|
43
|
-
# 4. Github sends callback to server, which sends the OK to client over websocket
|
44
|
-
|
45
|
-
# In leue of sessions, three global objects are used:
|
46
|
-
# Users: <Hash> with keys: <username> and vals: <Set(token)>
|
47
|
-
# AuthenticatedTokens: <hash> with keys: <token> and vals: <username>
|
48
|
-
# Sockets: <hash> with keys: <token> and vals: <socket>
|
49
|
-
|
50
|
-
Sockets = {}
|
51
|
-
AuthenticatedTokens = {}
|
52
15
|
Users = Hash.new { |hash, key| hash[key] = Set.new }
|
16
|
+
AuthenticatedTokens = {}
|
17
|
+
Sockets = Hash.new { |hash, key| hash[key] = Set.new }
|
18
|
+
|
19
|
+
CLIENT_BASE_URL = if ENV["RACK_ENV"] == "production"
|
20
|
+
"https://maxpleaner.github.io"
|
21
|
+
else
|
22
|
+
"http://localhost:8080"
|
23
|
+
end
|
53
24
|
|
54
25
|
class Server < Sinatra::Base
|
55
26
|
|
27
|
+
register Sinatra::ActiveRecordExtension
|
28
|
+
|
29
|
+
# Database config
|
30
|
+
if ENV["RACK_ENV"] == "production"
|
31
|
+
configure :production do
|
32
|
+
db = URI.parse(ENV['DATABASE_URL'] || 'postgres:///localhost/mydb')
|
33
|
+
ActiveRecord::Base.establish_connection(
|
34
|
+
:adapter => db.scheme == 'postgres' ? 'postgresql' : db.scheme,
|
35
|
+
:host => db.host,
|
36
|
+
:username => db.user,
|
37
|
+
:password => db.password,
|
38
|
+
:database => db.path[1..-1],
|
39
|
+
:encoding => 'utf8'
|
40
|
+
)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
set :database, {adapter: "sqlite3", database: "db.sqlite3"}
|
44
|
+
set :show_exceptions, true
|
45
|
+
end
|
46
|
+
|
56
47
|
set :server, 'thin'
|
57
48
|
Faye::WebSocket.load_adapter('thin')
|
58
49
|
|
59
|
-
# Allow some routes to be accessed from different origins
|
60
|
-
# This is unnecessary for websocket requests, since browsers don't implement
|
61
|
-
# the same restictions.
|
62
|
-
|
63
50
|
register Sinatra::CrossOrigin
|
64
51
|
|
65
|
-
# Github oAuth setup
|
66
|
-
# session is needed for the Github oAuth gem
|
67
|
-
# but its not used elsewhere
|
68
|
-
|
69
52
|
enable :sessions
|
70
53
|
set :github_options, {
|
71
54
|
scopes: "user",
|
@@ -74,49 +57,68 @@ class Server < Sinatra::Base
|
|
74
57
|
}
|
75
58
|
register Sinatra::Auth::Github
|
76
59
|
|
77
|
-
# ------------------------------------------------
|
78
|
-
# Standard HTTP routes
|
79
|
-
# (get '/ws' is the entrance to the websocket API)
|
80
|
-
# ------------------------------------------------
|
81
60
|
|
82
|
-
|
61
|
+
logged_in_only = Proc.new do |request|
|
62
|
+
if AuthenticatedTokens[request.params['token']]
|
63
|
+
false
|
64
|
+
else
|
65
|
+
{ error: ["not_authenticated for #{request.request_method} #{request.path_info}"] }.to_json
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
register Sinatra::CrudGenerator
|
70
|
+
crud_generate(
|
71
|
+
resource: "todo",
|
72
|
+
resource_class: Todo,
|
73
|
+
cross_origin_opts: {
|
74
|
+
allow_origin: CLIENT_BASE_URL
|
75
|
+
},
|
76
|
+
create: { auth: logged_in_only },
|
77
|
+
update: { auth: logged_in_only },
|
78
|
+
destroy: { auth: logged_in_only }
|
79
|
+
)
|
80
|
+
|
81
|
+
get '/health' do
|
82
|
+
cross_origin allow_origin: CLIENT_BASE_URL
|
83
|
+
status 200
|
84
|
+
end
|
83
85
|
|
84
86
|
get '/token' do
|
85
|
-
cross_origin allow_origin:
|
87
|
+
cross_origin allow_origin: CLIENT_BASE_URL
|
86
88
|
{ token: new_token }.to_json
|
87
89
|
end
|
88
90
|
|
89
|
-
# Then they send it in websocket connection request
|
90
|
-
# See server/lib/routes/ws.rb
|
91
|
-
|
92
91
|
get '/ws' do
|
93
|
-
|
92
|
+
Ws.run(request)
|
94
93
|
end
|
95
94
|
|
96
|
-
# Then they authenticate with Github
|
97
|
-
#
|
98
|
-
# If the client refreshes the page after logging in, they should have stored
|
99
|
-
# the token in a cookie. If they hit this route with the same token, it
|
100
|
-
# keeps them logged in.
|
101
|
-
#
|
102
|
-
# This needs to be clicked like a regular link - no AJAX
|
103
|
-
#
|
104
95
|
# TODO render a proper HTML page after authenticating not just plaintext
|
105
96
|
# saying they can close the window.
|
106
|
-
|
107
97
|
get '/authenticate' do
|
108
|
-
|
109
|
-
|
110
|
-
|
98
|
+
username = nil
|
99
|
+
token = params["token"]
|
100
|
+
if token
|
101
|
+
sockets = Sockets[token]
|
102
|
+
if sockets.any?
|
103
|
+
username = AuthenticatedTokens[token]
|
104
|
+
unless username
|
111
105
|
authenticate!
|
112
106
|
username = get_username
|
113
|
-
Users[username] << (token)
|
114
107
|
AuthenticatedTokens[token] = username
|
115
108
|
end
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
109
|
+
|
110
|
+
new_token = SecureRandom.urlsafe_base64
|
111
|
+
Sockets[new_token] = Sockets.delete(token)
|
112
|
+
AuthenticatedTokens[new_token] = AuthenticatedTokens.delete(token)
|
113
|
+
Users[username] << new_token
|
114
|
+
|
115
|
+
sockets.each do |socket|
|
116
|
+
socket.send({
|
117
|
+
action: "logged_in",
|
118
|
+
username: username,
|
119
|
+
new_token: new_token
|
120
|
+
}.to_json)
|
121
|
+
end
|
120
122
|
"authenticated as #{username}. (this window can be closed)"
|
121
123
|
else
|
122
124
|
"error. lost your websocket connection (this window can be closed)"
|
@@ -126,26 +128,22 @@ class Server < Sinatra::Base
|
|
126
128
|
end
|
127
129
|
end
|
128
130
|
|
129
|
-
# This is hit over AJAX
|
130
|
-
# This closes the websocket connection
|
131
|
-
# Clients should request a new token and reconnect to ws after logging out
|
132
|
-
|
133
131
|
get '/logout' do
|
134
|
-
cross_origin allow_origin:
|
132
|
+
cross_origin allow_origin: CLIENT_BASE_URL
|
135
133
|
token = params[:token]
|
136
134
|
if token
|
137
135
|
if username = AuthenticatedTokens[token]
|
138
136
|
logout!
|
139
137
|
AuthenticatedTokens.delete token
|
140
138
|
Users[username].delete token
|
141
|
-
Sockets[token].close
|
139
|
+
Sockets[token].each { |ws| ws.close(1000, "logged_out") }
|
142
140
|
Sockets.delete token
|
143
141
|
{ success: "logged out" }.to_json
|
144
142
|
else
|
145
|
-
{ error: "can't find user to log out" }.to_json
|
143
|
+
{ error: ["can't find user to log out"] }.to_json
|
146
144
|
end
|
147
145
|
else
|
148
|
-
{ error: 'cant log out; no token provided' }.to_json
|
146
|
+
{ error: ['cant log out; no token provided'] }.to_json
|
149
147
|
end
|
150
148
|
end
|
151
149
|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# included into ActiveRecord models
|
2
|
+
module ServerPush
|
3
|
+
|
4
|
+
# Can be overridden in model to limit who gets updates
|
5
|
+
# Returns a list of sockets (by default all of them)
|
6
|
+
def publish_to
|
7
|
+
Sockets.values.map(&:to_a).flatten
|
8
|
+
end
|
9
|
+
|
10
|
+
def save(*args)
|
11
|
+
should_push = valid? && !persisted?
|
12
|
+
result = super(*args)
|
13
|
+
if should_push
|
14
|
+
publish_to.each do |socket|
|
15
|
+
socket.send({
|
16
|
+
action: "add_record",
|
17
|
+
type: self.class.to_s.underscore,
|
18
|
+
record: public_attributes
|
19
|
+
}.to_json)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
def update(*args)
|
26
|
+
result = super(*args)
|
27
|
+
if result
|
28
|
+
publish_to.each do |socket|
|
29
|
+
socket.send({
|
30
|
+
action: "update_record",
|
31
|
+
type: self.class.to_s.underscore,
|
32
|
+
record: public_attributes
|
33
|
+
}.to_json)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def destroy(*args)
|
40
|
+
result = super(*args)
|
41
|
+
unless persisted?
|
42
|
+
publish_to.each do |socket|
|
43
|
+
socket.send({
|
44
|
+
action: "destroy_record",
|
45
|
+
type: self.class.to_s.underscore,
|
46
|
+
record: public_attributes
|
47
|
+
}.to_json)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
|
-
class
|
1
|
+
class Ws
|
2
2
|
|
3
|
-
# Opens a new websocket connection from a request
|
4
3
|
def self.run(request)
|
5
4
|
return unless Faye::WebSocket.websocket?(request.env)
|
6
5
|
socket = Faye::WebSocket.new(request.env)
|
@@ -13,7 +12,7 @@ class Routes::Ws
|
|
13
12
|
def self.onopen(request, ws)
|
14
13
|
token = request.params["token"]
|
15
14
|
if token
|
16
|
-
Sockets[token]
|
15
|
+
Sockets[token] << ws
|
17
16
|
else
|
18
17
|
ws.close
|
19
18
|
end
|
@@ -21,19 +20,26 @@ class Routes::Ws
|
|
21
20
|
|
22
21
|
def self.onmessage(request, ws, msg_data)
|
23
22
|
data = JSON.parse msg_data
|
24
|
-
|
25
|
-
|
23
|
+
token = data["token"]
|
24
|
+
user = find_username(token)
|
25
|
+
case data["action"]
|
26
|
+
when "try_authenticate" then try_authenticate(ws, token)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
29
30
|
def self.onclose(request, ws)
|
30
|
-
|
31
|
+
token = CGI.parse(URI.parse(ws.url).to_s)["token"]
|
32
|
+
Sockets.delete token
|
31
33
|
end
|
32
34
|
|
33
35
|
class << self
|
34
36
|
|
35
37
|
private
|
36
38
|
|
39
|
+
def find_username(token)
|
40
|
+
AuthenticatedTokens[token]
|
41
|
+
end
|
42
|
+
|
37
43
|
def try_authenticate(ws, token)
|
38
44
|
if username = AuthenticatedTokens[token]
|
39
45
|
ws.send({
|
@@ -47,11 +53,6 @@ class Routes::Ws
|
|
47
53
|
EM.next_tick { ws.send msg.to_json }
|
48
54
|
end
|
49
55
|
|
50
|
-
def delete_socket(request, ws)
|
51
|
-
token = CGI.parse(URI.parse(ws.url).to_s)["token"]
|
52
|
-
Sockets.delete token
|
53
|
-
end
|
54
|
-
|
55
56
|
end
|
56
57
|
|
57
58
|
end
|
data/lib/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinatra_sockets
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- maxpleaner
|
@@ -35,11 +35,17 @@ files:
|
|
35
35
|
- bin/sinatra_sockets
|
36
36
|
- lib/server_skeleton/Gemfile
|
37
37
|
- lib/server_skeleton/Gemfile.lock
|
38
|
+
- lib/server_skeleton/Procfile
|
38
39
|
- lib/server_skeleton/README.md
|
40
|
+
- lib/server_skeleton/Rakefile
|
39
41
|
- lib/server_skeleton/config.ru
|
40
|
-
- lib/server_skeleton/
|
41
|
-
- lib/server_skeleton/
|
42
|
+
- lib/server_skeleton/crud_generator.rb
|
43
|
+
- lib/server_skeleton/db/migrate/20170312215739_create_todos.rb
|
44
|
+
- lib/server_skeleton/db/schema.rb
|
45
|
+
- lib/server_skeleton/models.rb
|
42
46
|
- lib/server_skeleton/server.rb
|
47
|
+
- lib/server_skeleton/server_push.rb
|
48
|
+
- lib/server_skeleton/ws.rb
|
43
49
|
- lib/sinatra_sockets.rb
|
44
50
|
- lib/version.rb
|
45
51
|
homepage: http://github.com/maxpleaner/sinatra_sockets
|