Commit a78327b1 authored by Angel Li's avatar Angel Li
Browse files

Initial commit

parents
### Jetbrains ###
*.iml
.idea/
### Project ###
## Autogenerated Javascript files (since we use coffescript)
*.map
## Package config file needs to remain in JS for now ##
!package.js
## Dependency versions
.versions
# Mac OSX
.DS_Store
\ No newline at end of file
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/ejPSng | /bin/sh"
sudo: required
\ No newline at end of file
This diff is collapsed.
# Contributing Guidelines
If you're thinking about making a contribution to this project, then you're in the right place.
First, thank you for taking the time to contribute! Please give everything below a read before
attempting to contribute, as it may save you some time and energy when it comes time to submit
your awesome new feature, fix, or bug report!
The following contributions to Restivus are greatly appreciated:
- Code (via pull request)
- New or updated features
- Bug fixes
- Automated tests
- Documentation updates (currently via README)
- Bug reports (via GitHub Issues)
- Feature requests and voting (via GitHub Issues)
[GitHub Issues](https://github.com/kahmali/meteor-restivus/issues) are used for all bug and feature
tracking. [Milestones](https://github.com/kahmali/meteor-restivus/milestones) will be created for
each release version (e.g., `v1.0.0`), and any associated Issues or [Pull Requests]
(https://github.com/kahmali/meteor-restivus/pulls?q=is%3Aopen+is%3Apr) will be added to the
corresponding milestone.
## Code Contributions
Contributing code to an open source project can be fun and rewarding, especially when it's done
right. Check out the guidelines below for more information on getting your changes merged into a
release.
### Coding Conventions
Please adhere to the [Meteor Style Guide](https://github.com/meteor/meteor/wiki/Meteor-Style-Guide)
for all conventions not specified here:
1. 100 character line limit for all code, comments, and documentation files
### Pull Requests
All code contributions can be submitted via GitHub Pull Requests. Here are a few guidelines you
must adhere to when contributing to this project:
1. **All pull requests should be made on the `devel` branch, unless intended for a specific release!
In that case, they can be made on the branch matching the release version number (e.g.,
`1.0.0`)** If you're not familiar with [forks](https://help.github.com/articles/fork-a-repo/) and
[pull requests](https://help.github.com/articles/using-pull-requests/), please check out those
resources for more information.
1. Begin your feature branches from the latest version of `devel`.
1. Before submitting a pull request:
1. Rebase to the latest version of `devel`
1. Add automated tests to the `/tests` directory for any new features
1. Ensure all automated tests are passing by running `meteor test-packages ./` from the root
directory of the project and viewing the Tinytest output at `http://localhost:3000`
1. Update the [README](https://github.com/kahmali/meteor-restivus/blob/devel/README.md) and
[change log](https://github.com/kahmali/meteor-restivus/blob/devel/CHANGELOG.md) with any
corresponding changes
- Please follow the existing conventions within each document until detailed conventions can
be formalized for each
### Committing
Limit commits to one related set of changes. If you’ve worked on several without committing, use
[`git add -p`](http://nuclearsquid.com/writings/git-add/) to break it up into multiple commits.
Try to start and finish one related set of changes in a commit. If your set of changes spans
multiple commits, use interactive rebase [`git rebase -i`]
(https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase-i) to squash the commits
together.
### Commit Messages
Please follow these guidelines for commit messages:
1. Separate subject from body with a blank line
1. Limit the subject line to 72 characters (shoot for 50 to keep things concise, but use 72 as the
hard limit)
1. Capitalize the subject line
1. Do not end the subject line with a period
1. Use the imperative mood in the subject line
1. Wrap the body at 72 characters
1. Use the body to explain what and why vs. how
- _Note: Rarely, only the subject line is necessary_
For a detailed explanation, please see [How to Write a Git Commit Message]
(http://chris.beams.io/posts/git-commit/#seven-rules).
## Bug Reports
Please file all bug reports, no matter how big or small, as [GitHub Issues]
(https://github.com/kahmali/meteor-restivus/issues). Please provide details, and, if possible,
include steps to reproduce the bug, a sample GitHub repo with the bug reproduced, or sample code.
## Feature Requests
Restivus is still a work in progress. Feature requests are welcome, and can be created and voted on
using [GitHub Issues](https://github.com/kahmali/meteor-restivus/issues)!
\ No newline at end of file
The MIT License (MIT)
Copyright (c) 2014 Kahmali Rose
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
This diff is collapsed.
@Auth or= {}
###
A valid user will have exactly one of the following identification fields: id, username, or email
###
userValidator = Match.Where (user) ->
check user,
id: Match.Optional String
username: Match.Optional String
email: Match.Optional String
if _.keys(user).length is not 1
throw new Match.Error 'User must have exactly one identifier field'
return true
###
A password can be either in plain text or hashed
###
passwordValidator = Match.OneOf(String,
digest: String
algorithm: String)
###
Return a MongoDB query selector for finding the given user
###
getUserQuerySelector = (user) ->
if user.id
return {'_id': user.id}
else if user.username
return {'username': user.username}
else if user.email
return {'emails.address': user.email}
# We shouldn't be here if the user object was properly validated
throw new Error 'Cannot create selector from invalid user'
###
Log a user in with their password
###
@Auth.loginWithPassword = (user, password) ->
if not user or not password
throw new Meteor.Error 401, 'Unauthorized'
# Validate the login input types
check user, userValidator
check password, passwordValidator
# Retrieve the user from the database
authenticatingUserSelector = getUserQuerySelector(user)
authenticatingUser = Meteor.users.findOne(authenticatingUserSelector)
if not authenticatingUser
throw new Meteor.Error 401, 'Unauthorized'
if not authenticatingUser.services?.password
throw new Meteor.Error 401, 'Unauthorized'
# Authenticate the user's password
passwordVerification = Accounts._checkPassword authenticatingUser, password
if passwordVerification.error
throw new Meteor.Error 401, 'Unauthorized'
# Add a new auth token to the user's account
authToken = Accounts._generateStampedLoginToken()
hashedToken = Accounts._hashLoginToken authToken.token
Accounts._insertHashedLoginToken authenticatingUser._id, {hashedToken}
return {authToken: authToken.token, userId: authenticatingUser._id}
// We need a function that treats thrown errors exactly like Iron Router would.
// This file is written in JavaScript to enable copy-pasting Iron Router code.
// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L3
var env = process.env.NODE_ENV || 'development';
// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L47
ironRouterSendErrorToResponse = function (err, req, res) {
if (res.statusCode < 400)
res.statusCode = 500;
if (err.status)
res.statusCode = err.status;
if (env === 'development')
msg = (err.stack || err.toString()) + '\n';
else
//XXX get this from standard dict of error messages?
msg = 'Server error.';
console.error(err.stack || err.toString());
if (res.headersSent)
return req.socket.destroy();
res.setHeader('Content-Type', 'text/html');
res.setHeader('Content-Length', Buffer.byteLength(msg));
if (req.method === 'HEAD')
return res.end();
res.end(msg);
return;
}
class @Restivus
constructor: (options) ->
@_routes = []
@_config =
paths: []
useDefaultAuth: false
apiPath: 'api/'
version: null
prettyJson: false
auth:
token: 'services.resume.loginTokens.hashedToken'
user: ->
if @request.headers['x-auth-token']
token = Accounts._hashLoginToken @request.headers['x-auth-token']
userId: @request.headers['x-user-id']
token: token
defaultHeaders:
'Content-Type': 'application/json'
enableCors: true
# Configure API with the given options
_.extend @_config, options
if @_config.enableCors
corsHeaders =
'Access-Control-Allow-Origin': '*'
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
if @_config.useDefaultAuth
corsHeaders['Access-Control-Allow-Headers'] += ', X-User-Id, X-Auth-Token'
# Set default header to enable CORS if configured
_.extend @_config.defaultHeaders, corsHeaders
if not @_config.defaultOptionsEndpoint
@_config.defaultOptionsEndpoint = ->
@response.writeHead 200, corsHeaders
@done()
# Normalize the API path
if @_config.apiPath[0] is '/'
@_config.apiPath = @_config.apiPath.slice 1
if _.last(@_config.apiPath) isnt '/'
@_config.apiPath = @_config.apiPath + '/'
# URL path versioning is the only type of API versioning currently available, so if a version is
# provided, append it to the base path of the API
if @_config.version
@_config.apiPath += @_config.version + '/'
# Add default login and logout endpoints if auth is configured
if @_config.useDefaultAuth
@_initAuth()
else if @_config.useAuth
@_initAuth()
console.warn 'Warning: useAuth API config option will be removed in Restivus v1.0 ' +
'\n Use the useDefaultAuth option instead'
return this
###*
Add endpoints for the given HTTP methods at the given path
@param path {String} The extended URL path (will be appended to base path of the API)
@param options {Object} Route configuration options
@param options.authRequired {Boolean} The default auth requirement for each endpoint on the route
@param options.roleRequired {String or String[]} The default role required for each endpoint on the route
@param endpoints {Object} A set of endpoints available on the new route (get, post, put, patch, delete, options)
@param endpoints.<method> {Function or Object} If a function is provided, all default route
configuration options will be applied to the endpoint. Otherwise an object with an `action`
and all other route config options available. An `action` must be provided with the object.
###
addRoute: (path, options, endpoints) ->
# Create a new route and add it to our list of existing routes
route = new share.Route(this, path, options, endpoints)
@_routes.push(route)
route.addToApi()
return this
###*
Generate routes for the Meteor Collection with the given name
###
addCollection: (collection, options={}) ->
methods = ['get', 'post', 'put', 'patch', 'delete', 'getAll']
methodsOnCollection = ['post', 'getAll']
# Grab the set of endpoints
if collection is Meteor.users
collectionEndpoints = @_userCollectionEndpoints
else
collectionEndpoints = @_collectionEndpoints
# Flatten the options and set defaults if necessary
endpointsAwaitingConfiguration = options.endpoints or {}
routeOptions = options.routeOptions or {}
excludedEndpoints = options.excludedEndpoints or []
# Use collection name as default path
path = options.path or collection._name
# Separate the requested endpoints by the route they belong to (one for operating on the entire
# collection and one for operating on a single entity within the collection)
collectionRouteEndpoints = {}
entityRouteEndpoints = {}
if _.isEmpty(endpointsAwaitingConfiguration) and _.isEmpty(excludedEndpoints)
# Generate all endpoints on this collection
_.each methods, (method) ->
# Partition the endpoints into their respective routes
if method in methodsOnCollection
_.extend collectionRouteEndpoints, collectionEndpoints[method].call(this, collection)
else _.extend entityRouteEndpoints, collectionEndpoints[method].call(this, collection)
return
, this
else
# Generate any endpoints that haven't been explicitly excluded
_.each methods, (method) ->
if method not in excludedEndpoints and endpointsAwaitingConfiguration[method] isnt false
# Configure endpoint and map to it's http method
# TODO: Consider predefining a map of methods to their http method type (e.g., getAll: get)
endpointOptions = endpointsAwaitingConfiguration[method]
configuredEndpoint = {}
_.each collectionEndpoints[method].call(this, collection), (action, methodType) ->
configuredEndpoint[methodType] =
_.chain action
.clone()
.extend endpointOptions
.value()
# Partition the endpoints into their respective routes
if method in methodsOnCollection
_.extend collectionRouteEndpoints, configuredEndpoint
else _.extend entityRouteEndpoints, configuredEndpoint
return
, this
# Add the routes to the API
@addRoute path, routeOptions, collectionRouteEndpoints
@addRoute "#{path}/:id", routeOptions, entityRouteEndpoints
return this
###*
A set of endpoints that can be applied to a Collection Route
###
_collectionEndpoints:
get: (collection) ->
get:
action: ->
entity = collection.findOne @urlParams.id
if entity
{status: 'success', data: entity}
else
statusCode: 404
body: {status: 'fail', message: 'Item not found'}
put: (collection) ->
put:
action: ->
entityIsUpdated = collection.update @urlParams.id, @bodyParams
if entityIsUpdated
entity = collection.findOne @urlParams.id
{status: 'success', data: entity}
else
statusCode: 404
body: {status: 'fail', message: 'Item not found'}
patch: (collection) ->
patch:
action: ->
entityIsUpdated = collection.update @urlParams.id, $set: @bodyParams
if entityIsUpdated
entity = collection.findOne @urlParams.id
{status: 'success', data: entity}
else
statusCode: 404
body: {status: 'fail', message: 'Item not found'}
delete: (collection) ->
delete:
action: ->
if collection.remove @urlParams.id
{status: 'success', data: message: 'Item removed'}
else
statusCode: 404
body: {status: 'fail', message: 'Item not found'}
post: (collection) ->
post:
action: ->
entityId = collection.insert @bodyParams
entity = collection.findOne entityId
if entity
statusCode: 201
body: {status: 'success', data: entity}
else
statusCode: 400
body: {status: 'fail', message: 'No item added'}
getAll: (collection) ->
get:
action: ->
entities = collection.find().fetch()
if entities
{status: 'success', data: entities}
else
statusCode: 404
body: {status: 'fail', message: 'Unable to retrieve items from collection'}
###*
A set of endpoints that can be applied to a Meteor.users Collection Route
###
_userCollectionEndpoints:
get: (collection) ->
get:
action: ->
entity = collection.findOne @urlParams.id, fields: profile: 1
if entity
{status: 'success', data: entity}
else
statusCode: 404
body: {status: 'fail', message: 'User not found'}
put: (collection) ->
put:
action: ->
entityIsUpdated = collection.update @urlParams.id, $set: profile: @bodyParams
if entityIsUpdated
entity = collection.findOne @urlParams.id, fields: profile: 1
{status: "success", data: entity}
else
statusCode: 404
body: {status: 'fail', message: 'User not found'}
delete: (collection) ->
delete:
action: ->
if collection.remove @urlParams.id
{status: 'success', data: message: 'User removed'}
else
statusCode: 404
body: {status: 'fail', message: 'User not found'}
post: (collection) ->
post:
action: ->
# Create a new user account
entityId = Accounts.createUser @bodyParams
entity = collection.findOne entityId, fields: profile: 1
if entity
statusCode: 201
body: {status: 'success', data: entity}
else
statusCode: 400
{status: 'fail', message: 'No user added'}
getAll: (collection) ->
get:
action: ->
entities = collection.find({}, fields: profile: 1).fetch()
if entities
{status: 'success', data: entities}
else
statusCode: 404
body: {status: 'fail', message: 'Unable to retrieve users'}
###
Add /login and /logout endpoints to the API
###
_initAuth: ->
self = this
###
Add a login endpoint to the API
After the user is logged in, the onLoggedIn hook is called (see Restfully.configure() for
adding hook).
###
@addRoute 'login', {authRequired: false},
post: ->
# Grab the username or email that the user is logging in with
user = {}
if @bodyParams.user
if @bodyParams.user.indexOf('@') is -1
user.username = @bodyParams.user
else
user.email = @bodyParams.user
else if @bodyParams.username
user.username = @bodyParams.username
else if @bodyParams.email
user.email = @bodyParams.email
password = @bodyParams.password
if @bodyParams.hashed
password =
digest: password
algorithm: 'sha-256'
# Try to log the user into the user's account (if successful we'll get an auth token back)
try
auth = Auth.loginWithPassword user, password
catch e
return {} =
statusCode: e.error
body: status: 'error', message: e.reason
# Get the authenticated user
# TODO: Consider returning the user in Auth.loginWithPassword(), instead of fetching it again here
if auth.userId and auth.authToken
searchQuery = {}
searchQuery[self._config.auth.token] = Accounts._hashLoginToken auth.authToken
@user = Meteor.users.findOne
'_id': auth.userId
searchQuery
@userId = @user?._id
response = {status: 'success', data: auth}
# Call the login hook with the authenticated user attached
extraData = self._config.onLoggedIn?.call(this)
if extraData?
_.extend(response.data, {extra: extraData})
response
logout = ->
# Remove the given auth token from the user's account
authToken = @request.headers['x-auth-token']
hashedToken = Accounts._hashLoginToken authToken
tokenLocation = self._config.auth.token
index = tokenLocation.lastIndexOf '.'
tokenPath = tokenLocation.substring 0, index
tokenFieldName = tokenLocation.substring index + 1
tokenToRemove = {}
tokenToRemove[tokenFieldName] = hashedToken
tokenRemovalQuery = {}
tokenRemovalQuery[tokenPath] = tokenToRemove
Meteor.users.update @user._id, {$pull: tokenRemovalQuery}
response = {status: 'success', data: {message: 'You\'ve been logged out!'}}
# Call the logout hook with the authenticated user attached
extraData = self._config.onLoggedOut?.call(this)
if extraData?
_.extend(response.data, {extra: extraData})
response
###
Add a logout endpoint to the API
After the user is logged out, the onLoggedOut hook is called (see Restfully.configure() for
adding hook).
###
@addRoute 'logout', {authRequired: true},
get: ->
console.warn "Warning: Default logout via GET will be removed in Restivus v1.0. Use POST instead."
console.warn " See https://github.com/kahmali/meteor-restivus/issues/100"
return logout.call(this)
post: logout
Restivus = @Restivus
class share.Route
constructor: (@api, @path, @options, @endpoints) ->
# Check if options were provided
if not @endpoints
@endpoints = @options
@options = {}