sinatra_sockets 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|