Sinatra sockets
about
A boilerplate including:
- Sinatra
- Github oAuth
- Faye websockets, with token (not session) based auth so the front-end can be on a different host.
- ActiveRecord
- CRUD generator for routes
- Server-push module to alert clients of db updates.
Note that at this point this project is the exact same as the server component of vue-sinatra-boiler. Originally this included a minimal front-end, but upstream development ended up happening on an API-only variant, and that was the most full-featured version.
In the future I would like to add generator options for components that are not necessarily dependent on one another, such as:
- whether or not to include simple (no auth) front end demo page for websockets
- whether or not to include Activerecord with crud generator and server-push module
- Whether or not to add oAuth with token-based websocket credentials
installing
$ gem install sinatra_sockets
$ sinatra_sockets generate .
It will print the generated directory tree:
.
├── config.ru
├── crud_generator.rb
├── db
│ ├── migrate
│ │ └── 20170312215739_create_todos.rb
│ └── schema.rb
├── Gemfile
├── Gemfile.lock
├── models.rb
├── Procfile
├── Rakefile
├── README.md
├── server_push.rb
├── server.rb
└── ws.rb
2 directories, 13 files
Go to Github settings and create an oAuth application
Set up the server:
cd server_skeleton
bundle install
bundle exec rake db:create db:migrate
cp .env.example .env
nano .env # add the github credentials here
Start the server with thin (port defaults to 3000):
bundle exec thin start -p 3000
components in detail:
database / models
this uses sinatra-activerecord
and is similar to what's found in rails.
To generate a migration: bundle exec rake db:create_migration NAME=my_migration_name
Then customize it and bundle exec rake db:migrate
.
server.rb
contains the database configuration inline in the ruby code.
Models are all listed in models.rb
, but it isn't necessary to do so. There are
no dynamic requires happening in this application - everything is individually
required in server.rb
.
If using the crud generator or server-push, there is one required method that all
models must respond to: public_attributes
. It returns a hash which is a filtered
version of attributes
(suitable to be sent to clients).
auth
Here's how the auth flow works (the pieces are in server.rb
and ws.rb
):
-
- Client checks their cookies to see if there's a token.
- If there's not, one is requested from the server and saved as a cookie.
- Client establishes a websocket connection with server, passing token in query params
-
- If the client found the token in the query, it will send a websocket request to try and authenticate.
- If the server had stored credentials for that token, the client get a success response over websockets.
- otherwise the client will get no response
- If the client had to fetch a new token, the authentication is put on hold until the user clicks the
"authenticate with github" button, passing the token to the server
- The server makes the client through the Github auth flow but stores the token ref.
- When Github auth is done clients get a ws message (looked up by token) saying they're authenticated.
- If the client found the token in the query, it will send a websocket request to try and authenticate.
crud generator
In the Sinatra server definition in server.rb
, it's included with
register Sinatra::CrudGenerator
. This exposes one behemoth of a method:
crud_generate
.
Here's its annotated signature (all the options are keyword args and are optional unless stated)
def crud_generate(
# String such as "todo" (required).
# The singular and pluralized forms are used to define routes i.e. /todos or /todo
resource:,
# Class such as Todo (required)
resource_class:,
# String such as '/api/', prefixed on all routes (defaults to '/')
root_path: '/',
# Hash. If this is defined, the `cross_origin` method is invoked with it as an arument.
# e.g. { cross_origin: "http://localhost:8080" }
# would allow all these CRUD routes to be hit by this origin.
cross_origin_opts: nil,
# A Proc which is passed the request and returns something truthy if the auth failed.
# For example if it returns { error: "bad reqeust" }.to_json that will be the
# final return value of the route and it will return early.
# If provided, this proc will apply to all of the routes, though each route can have
# it's own proc which will override it (see below)
auth: nil,
# Each route has its own option which is a hash for configuration.
# These are optional and defaults are set (see crud_generator.rb).
# There are slight differences in the configuration hashes accepted per route,
# but here is the full list of keys (all are optional):
#
# method: Symbol (i.e. :get or :post, must be supported by Sinatra)
#
# path: String (defaults to "/#{resource}" or "/#{plural_resource}"
#
# auth: Proc with the same signature as the one discussed before
#
# filter: Proc which is used on index route. Is passed an ActiveRecord Query of all records
# of the record_class and returns some subset which gets sent to the client.
#
# secure_params: Proc used on create/update which is passed the request object and returns
# a list of the accepted param keys (params are filtered to only include these)
#
# Hash with :method, :path, :auth, and :filter keys
index: nil,
# Hash with :method, :path, :auth, and :secure_params keys
create: nil,
# Hash with :method, :path, and :auth keys
read: nil,
# hash with :method, :path, :auth, and :secure_params keys
update: nil,
# hash with :method, :path, and :auth keys
destroy: nil,
# Array of symbols such as [:create, :update], will skip generating these routes.
except: []
)
Server push
This is a module which can be include
d into any model.
It patches three foundational methods in ActiveRecord:
-
save
handles the case of creating new records.- although this method is called under the hood by both
create
andupdate
, only thecreate
case is handled here because there's a check whether the record is persisted and previously unsaved.
- although this method is called under the hood by both
-
update
- handles when existing records are changed -
destroy
- handles when records are deleted.
So after include ServerPush
is placed in the model, any calls to create
(or save
) on an unsaved record will trigger a "add_record" ws message to be sent.
Any call to destroy
will trigger "destroy_record", and similarly update
triggers
"update_record"
By default all sockets get these messages sent to them, though this can be configured
by defining a publish_to
method in the model that returns a list of sockets.
All of the outgoing websocket messages in server-push are hashes with this signature:
- action: string ("add_record", "update_record", or "destroy_record")
- type: string i.e. "todo"
- record: Hash with the model's
public_attributes
.
vue-sinatra-boiler has the front-end API in place to use this.