From 0edcfac46bc5be2ef922262841d1cd097d571f0e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 1 Jan 2014 14:20:01 -0700 Subject: [PATCH 1/7] #2217 start adding APIv2 generated documentation via swagger-node-express --- bower.json | 6 +- package.json | 3 +- src/apidoc.coffee | 225 ++++++++++++++++++++++++++++++++++++++++++ src/routes/pages.js | 4 + src/server.js | 4 + views/static/api.jade | 69 +++++++++++++ 6 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 src/apidoc.coffee create mode 100644 views/static/api.jade diff --git a/bower.json b/bower.json index 2769a82370..8afe312ae1 100644 --- a/bower.json +++ b/bower.json @@ -38,13 +38,13 @@ "gemoji": "git://github.com/github/gemoji", "sticky": "*", "bootstrap-tour": "~0.8.0", - "angular-ui-utils": "~0.1.0" + "angular-ui-utils": "~0.1.0", + "swagger-ui": "~2.0.3" }, "resolutions": { "jquery": "~2.0.3", "bootstrap": "v2.3.2", - "angular": "~1.2.1", - "angular-ui-utils": "~0.1.0" + "angular": "~1.2.1" }, "devDependencies": { "angular-mocks": "~1.2.1" diff --git a/package.json b/package.json index 9e2ac50493..a187a7f85d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", "js2xmlparser": "~0.1.2", "mongoose": "~3.8.1", - "domain-middleware": "~0.1.0" + "domain-middleware": "~0.1.0", + "swagger-node-express": "~1.3.2" }, "private": true, "subdomain": "habitrpg", diff --git a/src/apidoc.coffee b/src/apidoc.coffee new file mode 100644 index 0000000000..430712519d --- /dev/null +++ b/src/apidoc.coffee @@ -0,0 +1,225 @@ +# see https://github.com/wordnik/swagger-node-express + +_ = require('lodash') + +module.exports = (swagger) -> + + swagger.configureSwaggerPaths("", "/api-docs", "") + + api = + "/content": + description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon)." + method: 'GET' + + "/export/history": + description: "Export user history" + method: 'GET' + + # --------------------------------- + # User + # --------------------------------- + + # Scoring + + "/user/tasks/{id}/{direction}": + description: "Simple scoring of a task" + params: [ + swagger.pathParam("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") + swagger.pathParam("direction", "Either 'up' or 'down'", "string") + ] + method: 'POST' + + # Tasks + "/user/tasks": + description: "Get all user's tasks" + + "/user/tasks/{id}": + description: "Get an individual task" + params: [ + swagger.pathParam("id", "Task ID", "string") + ] + + "/user/tasks/{id}": + description: "Update a user's task" + method: 'PUT' + params: [ + swagger.pathParam("id", "Task ID", "string") + ] + body: [ + swagger.bodyParam("task","Send up the whole task","string") + ] + + "/user/tasks/{id}": + method: 'DELETE' + "/user/tasks": + method: 'POST' + #body={} + "/user/tasks/{id}/sort": + method: 'POST' + #query={to,from} + "/user/tasks/clear-completed": + method: 'POST' + "/user/tasks/{id}/unlink": + method: 'POST' + + # Inventory + "/user/inventory/buy/{key}": + method: 'POST' + "/user/inventory/sell/{type}/{key}": + method: 'POST' + "/user/inventory/purchase/{type}/{key}": + method: 'POST' + "/user/inventory/feed/{pet}/{food}": + method: 'POST' + "/user/inventory/equip/{type}/{key}": + method: 'POST' + "/user/inventory/hatch/{egg}/{hatchingPotion}": + method: 'POST' + + # User + "/user:GET": + path: '/user' + "/user:PUT": + path: '/user' + method: 'PUT' + # body={} + "/user:DELETE": + path: '/user' + method: 'DELETE' + "/user/revive": + method: 'POST' + "/user/reroll": + method: 'POST' + "/user/reset": + method: 'POST' + "/user/sleep": + method: 'POST' + "/user/rebirth": + method: 'POST' + "/user/class/change": + method: 'POST' + #query={class} + "/user/class/allocate": + method: 'POST' + #query={stat} + "/user/class/cast/:spell": + method: 'POST' + "/user/unlock": + method: 'POST' + "/user/buy-gems": + method: 'POST' + "/user/batch-update": + method: 'POST' + + # Tags + "/user/tags": + method: 'POST' + #body={} + "/user/tags/{id}:PUT": + path: 'user/tags/{id}' + method: 'PUT' + #body={} + "/user/tags/{id}:DELETE": + path: 'user/tags/{id}' + method: 'DELETE' + + # --------------------------------- + # Groups + # --------------------------------- + "/groups:GET": + path: '/groups' + "/groups:POST": + path: '/groups' + method: 'POST' + "/groups/{gid}:GET": + path: '/groups/{gid}' + "/groups/{gid}:POST": + path: '/groups/{gid}' + method: 'POST' + "/groups/{gid}": + path: '/groups/{gid}' + method: 'PUT' + + "/groups/{gid}/join": + method: 'POST' + "/groups/{gid}/leave": + method: 'POST' + "/groups/{gid}/invite": + method: 'POST' + "/groups/{gid}/removeMember": + method: 'POST' + "/groups/{gid}/questAccept": + method: 'POST' + # query={key} (optional. if provided, trigger new invite, if not, accept existing invite) + "/groups/{gid}/questReject": + method: 'POST' + "/groups/{gid}/questAbort": + method: 'POST' + + #GET /groups/:gid/chat + "/groups/{gid}/chat": + method: 'POST' + "/groups/{gid}/chat/{messageId}": + method: 'DELETE' + + + # --------------------------------- + # Members + # --------------------------------- + "/members/{uid}":{} + + # --------------------------------- + # Challenges + # --------------------------------- + + # Note: while challenges belong to groups, and would therefore make sense as a nested resource + # (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab + # without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource + "/challenges:GET": + path: '/challenges' + "/challenges:POST": + path: '/challenges' + method: 'POST' + "/challenges/{cid}:GET": {} + "/challenges/{cid}:POST": + path: '/challenges/{cid}' + method: 'POST' + "/challenges/{cid}:DELETE": + path: '/challenges/{cid}' + method: 'DELETE' + "/challenges/{cid}/close": + method: 'POST' + "/challenges/{cid}/join": + method: 'POST' + "/challenges/{cid}/leave": + method: 'POST' + "/challenges/{cid}/member/{uid}":{} + + _.each api, (spec, path) -> + ## Spec format is: + # spec: + # path: "/pet/{petId}" + # description: "Operations about pets" + # notes: "Returns a pet based on ID" + # summary: "Find pet by ID" + # method: "GET" + # params: [swagger.pathParam("petId", "ID of pet that needs to be fetched", "string")] + # type: "Pet" + # errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")] + # nickname: "getPetById" + + spec.description ?= '' + _.defaults spec, + path: path + nickname: path + notes: spec.description + summary: spec.description + params: [] + #type: 'Pet' + errorResponses: [] + method: 'GET' + route = {spec} + console.log(spec.params) + swagger["add#{route.spec.method}"](route);true + + swagger.configure("http://localhost:3000", "0.1") \ No newline at end of file diff --git a/src/routes/pages.js b/src/routes/pages.js index 72e3f4fb10..de22cf95c8 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -31,6 +31,10 @@ router.get('/static/terms', middleware.locals, function(req, res) { res.render('static/terms', {env: res.locals.habitrpg}); }); +router.get('/static/api', middleware.locals, function(req, res) { + res.render('static/api', {env: res.locals.habitrpg}); +}); + // --------- Redirects -------- router.get('/splash.html', function(req, res) { diff --git a/src/server.js b/src/server.js index 91fa6ea9a9..3d5ad385a2 100644 --- a/src/server.js +++ b/src/server.js @@ -8,6 +8,7 @@ var nconf = require('nconf'); var utils = require('./utils'); var middleware = require('./middleware'); var domainMiddleware = require('domain-middleware'); +var swagger = require("swagger-node-express"); var server; var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; @@ -110,6 +111,9 @@ app.use('/export', require('./routes/dataexport').middleware); app.use(utils.errorHandler); +swagger.setAppHandler(app); +require('./apidoc.coffee')(swagger); + server = http.createServer(app).listen(app.get("port"), function() { return console.log("Express server listening on port " + app.get("port")); }); diff --git a/views/static/api.jade b/views/static/api.jade new file mode 100644 index 0000000000..ad44676b3b --- /dev/null +++ b/views/static/api.jade @@ -0,0 +1,69 @@ +!!! 5 +html + head + title Swagger UI + link(href='//fonts.googleapis.com/css?family=Droid+Sans:400,700', rel='stylesheet', type='text/css') + link(href='/bower_components/swagger-ui/dist/css/highlight.default.css', media='screen', rel='stylesheet', type='text/css') + link(href='/bower_components/swagger-ui/dist/css/screen.css', media='screen', rel='stylesheet', type='text/css') + script(src='/bower_components/swagger-ui/dist/lib/shred.bundle.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery-1.8.0.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery.slideto.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery.wiggle.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery.ba-bbq.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/handlebars-1.0.0.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/underscore-min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/backbone-min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/swagger.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/swagger-ui.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/highlight.7.3.pack.js', type='text/javascript') + script(type='text/javascript') + $(function () { + window.swaggerUi = new SwaggerUi({ + url: "/api-docs", + dom_id: "swagger-ui-container", + supportedSubmitMethods: ['get', 'post', 'put', 'delete'], + onComplete: function(swaggerApi, swaggerUi){ + if(console) { + console.log("Loaded SwaggerUI") + } + $('pre code').each(function(i, e) {hljs.highlightBlock(e)}); + }, + onFailure: function(data) { + if(console) { + console.log("Unable to Load SwaggerUI"); + console.log(data); + } + }, + docExpansion: "none" + }); + + $('#input_apiKey').change(function() { + var key = $('#input_apiKey')[0].value; + console.log("key: " + key); + if(key && key.trim() != "") { + console.log("added key " + key); + window.authorizations.add("key", new ApiKeyAuthorization("api_key", key, "query")); + } + }) + window.swaggerUi.load(); + }); + body + #header + .swagger-ui-wrap + a#logo(href='http://swagger.wordnik.com') swagger + //-form#api_selector + //- +
+ +
+
+ +
+ .input + input#input_baseUrl(placeholder='http://example.com/api', name='baseUrl', type='text') + .input + input#input_apiKey(placeholder='api_key', name='apiKey', type='text') + .input + a#explore(href='#') Explore + #message-bar.swagger-ui-wrap + #swagger-ui-container.swagger-ui-wrap From f676dba15944a45c4bd846f772487ff5feb9e537 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 1 Jan 2014 16:23:06 -0700 Subject: [PATCH 2/7] #2217 more progress documenting API --- src/apidoc.coffee | 141 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 122 insertions(+), 19 deletions(-) diff --git a/src/apidoc.coffee b/src/apidoc.coffee index 430712519d..4eb05a5d26 100644 --- a/src/apidoc.coffee +++ b/src/apidoc.coffee @@ -1,8 +1,10 @@ # see https://github.com/wordnik/swagger-node-express _ = require('lodash') +content = require('habitrpg-shared').content module.exports = (swagger) -> + [path,body,query] = [swagger.pathParam, swagger.bodyParam, swagger.queryParam] swagger.configureSwaggerPaths("", "/api-docs", "") @@ -24,8 +26,8 @@ module.exports = (swagger) -> "/user/tasks/{id}/{direction}": description: "Simple scoring of a task" params: [ - swagger.pathParam("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") - swagger.pathParam("direction", "Either 'up' or 'down'", "string") + path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") + path("direction", "Either 'up' or 'down'", "string") ] method: 'POST' @@ -36,92 +38,194 @@ module.exports = (swagger) -> "/user/tasks/{id}": description: "Get an individual task" params: [ - swagger.pathParam("id", "Task ID", "string") + path("id", "Task ID", "string") ] "/user/tasks/{id}": description: "Update a user's task" method: 'PUT' params: [ - swagger.pathParam("id", "Task ID", "string") - ] - body: [ - swagger.bodyParam("task","Send up the whole task","string") + path("id", "Task ID", "string") + body("","Send up the whole task","object") ] "/user/tasks/{id}": + description: "Delete a task" method: 'DELETE' + params: [ path("id", "Task ID", "string") ] + "/user/tasks": + description: "Create a task" method: 'POST' - #body={} + params: [ body("","Send up the whole task","object") ] + "/user/tasks/{id}/sort": method: 'POST' - #query={to,from} + description: 'Sort tasks' + params: [ + path("id", "Task ID", "string") + query("from","Index where you're sorting from (0-based)","integer") + query("to","Index where you're sorting to (0-based)","integer") + ] + "/user/tasks/clear-completed": method: 'POST' + description: "Clears competed To-Dos (needed periodically for performance." + "/user/tasks/{id}/unlink": method: 'POST' + description: 'Unlink a task from its challenge' + # TODO query params? + params: [path("id", "Task ID", "string")] + # Inventory "/user/inventory/buy/{key}": method: 'POST' + description: "Buy a gear piece and equip it automatically" + params:[ + path 'key',"The key of the item to buy (call /content route for available keys)",'string', _.keys(content.gear.flat) + #TODO embed keys + ] + "/user/inventory/sell/{type}/{key}": method: 'POST' + description: "Sell inventory items back to Alexander" + params: [ + #TODO verify these are the correct types + path('type',"The type of object you're selling back.",'string',['gear','eggs','hatchingPotions','food']) + path('key',"The object key you're selling back (call /content route for available keys)",'string') + ] + "/user/inventory/purchase/{type}/{key}": method: 'POST' + description: "Purchase a gem-purchaseable item from Alexander" + params:[ + path('type',"The type of object you're purchasing.",'string',['gear','eggs','hatchingPotions','food']) + path('key',"The object key you're purchasing (call /content route for available keys)",'string') + ] + "/user/inventory/feed/{pet}/{food}": method: 'POST' + description: "Feed your pet some food" + params: [ + path 'pet',"The key of the pet you're feeding",'string'#,_.keys(content.pets)) + path 'food',"The key of the food to feed your pet",'string',_.keys(content.food) + ] + "/user/inventory/equip/{type}/{key}": method: 'POST' + description: "Equip an item (either pets, mounts, or gear)" + params: [ + path 'type',"Type to equip",'string',['pets','mounts','gear'] + path 'key',"The object key you're equipping (call /content route for available keys)",'string' + ] + "/user/inventory/hatch/{egg}/{hatchingPotion}": method: 'POST' + description: "Pour a hatching potion on an egg" + params: [ + path 'egg',"The egg key to hatch",'string',_.keys(content.eggs) + path 'hatchingPotion',"The hatching potion to pour",'string',_.keys(content.hatchingPotions) + ] # User "/user:GET": path: '/user' + description: "Get the full user object" + "/user:PUT": path: '/user' method: 'PUT' - # body={} + description: "Update the user object (only certain attributes are supported)" + params: [ + body '','The user object','object' + ] + "/user:DELETE": path: '/user' method: 'DELETE' + description: "Delete a user object entirely, USE WITH CAUTION!" + "/user/revive": method: 'POST' + description: "Revive your dead user" + "/user/reroll": method: 'POST' + description: 'Drink the Fortify Potion (Note, it used to be called re-roll)' + "/user/reset": method: 'POST' + description: "Completely reset your account" + "/user/sleep": method: 'POST' + description: "Toggle whether you're resting in the inn" + "/user/rebirth": method: 'POST' + description: "Rebirth your avatar" + "/user/class/change": method: 'POST' - #query={class} + description: "Either remove your avatar's class, or change it to something new" + params: [ + query 'class',"The key of the class to change to. If not provided, user's class is removed.",'string',['warrior','healer','rogue','wizard',''] + ] + "/user/class/allocate": method: 'POST' - #query={stat} - "/user/class/cast/:spell": + description: "Allocate one point towards an attribute" + params: [ + query 'stat','The stat to allocate towards','string' + ] + + "/user/class/cast/{spell}": method: 'POST' + description: "Cast a spell" + #TODO finish + "/user/unlock": method: 'POST' - "/user/buy-gems": - method: 'POST' + description: "Unlock a certain gem-purchaseable path (or multiple paths)" + params: [ + query 'path',"The path to unlock, such as hair.green or shirts.red,shirts.blue",'string' + ] + "/user/batch-update": method: 'POST' + description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',params:{},body:{},query:{}}" + params:[ + body '','The array of batch-operations to perform','object' + ] # Tags "/user/tags": method: 'POST' - #body={} + description: 'Create a new tag' + params: [ + #TODO document + body '','New tag','object' + ] + "/user/tags/{id}:PUT": path: 'user/tags/{id}' method: 'PUT' - #body={} + description: "Edit a tag" + params: [ + path 'id','The id of the tag to edit','string' + body '','Tag edits','object' + ] + "/user/tags/{id}:DELETE": path: 'user/tags/{id}' method: 'DELETE' + description: 'Delete a tag' + params: [ + path 'id','Id of tag to delete','string' + ] + # --------------------------------- # Groups @@ -203,7 +307,7 @@ module.exports = (swagger) -> # notes: "Returns a pet based on ID" # summary: "Find pet by ID" # method: "GET" - # params: [swagger.pathParam("petId", "ID of pet that needs to be fetched", "string")] + # params: [path("petId", "ID of pet that needs to be fetched", "string")] # type: "Pet" # errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")] # nickname: "getPetById" @@ -219,7 +323,6 @@ module.exports = (swagger) -> errorResponses: [] method: 'GET' route = {spec} - console.log(spec.params) swagger["add#{route.spec.method}"](route);true swagger.configure("http://localhost:3000", "0.1") \ No newline at end of file From 98361ea94fecdb3f431d53a55e42e5d5a6419ae2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 1 Jan 2014 16:37:03 -0700 Subject: [PATCH 3/7] #2217 add note about API-docs being a WIP --- views/static/api.jade | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/views/static/api.jade b/views/static/api.jade index ad44676b3b..aa89f44b51 100644 --- a/views/static/api.jade +++ b/views/static/api.jade @@ -50,7 +50,7 @@ html body #header .swagger-ui-wrap - a#logo(href='http://swagger.wordnik.com') swagger + a#logo(href='http://swagger.wordnik.com') HabitRPG API Documentation (Under Construction) //-form#api_selector //-
@@ -65,5 +65,9 @@ html input#input_apiKey(placeholder='api_key', name='apiKey', type='text') .input a#explore(href='#') Explore + + .swagger-ui-wrap(style='padding:50px') + p(style='color:red') This documentation is a work in progress. If you'd like to help finish it, see #2217. + #message-bar.swagger-ui-wrap #swagger-ui-container.swagger-ui-wrap From 11d3b7473a40e027c20be0265c77297fbe62cbef Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 1 Jan 2014 16:53:42 -0700 Subject: [PATCH 4/7] #2217 use BASE_URL for swagger config --- src/apidoc.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apidoc.coffee b/src/apidoc.coffee index 4eb05a5d26..46c9e99fd6 100644 --- a/src/apidoc.coffee +++ b/src/apidoc.coffee @@ -1,6 +1,7 @@ # see https://github.com/wordnik/swagger-node-express _ = require('lodash') +nconf = require('nconf') content = require('habitrpg-shared').content module.exports = (swagger) -> @@ -325,4 +326,4 @@ module.exports = (swagger) -> route = {spec} swagger["add#{route.spec.method}"](route);true - swagger.configure("http://localhost:3000", "0.1") \ No newline at end of file + swagger.configure(nconf.get('BASE_URL'), "0.1") \ No newline at end of file From a2b30dcb1a42a61dce75915892376001cc87aecb Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 1 Jan 2014 20:04:48 -0700 Subject: [PATCH 5/7] #2217 merge apiv2 route definitions with swagger decorations --- bower.json | 2 +- package.json | 2 +- src/apidoc.coffee | 329 ------------------------- src/routes/apiv2.coffee | 525 ++++++++++++++++++++++++++++++++++++++++ src/routes/apiv2.js | 140 ----------- src/server.js | 6 +- views/static/api.jade | 2 +- 7 files changed, 531 insertions(+), 475 deletions(-) delete mode 100644 src/apidoc.coffee create mode 100644 src/routes/apiv2.coffee delete mode 100644 src/routes/apiv2.js diff --git a/bower.json b/bower.json index 8afe312ae1..d66189fe30 100644 --- a/bower.json +++ b/bower.json @@ -39,7 +39,7 @@ "sticky": "*", "bootstrap-tour": "~0.8.0", "angular-ui-utils": "~0.1.0", - "swagger-ui": "~2.0.3" + "swagger-ui": "https://github.com/wordnik/swagger-ui.git#105c516f5f055b140e935e9cfe0c36f841921dff" }, "resolutions": { "jquery": "~2.0.3", diff --git a/package.json b/package.json index a187a7f85d..bd31016872 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "js2xmlparser": "~0.1.2", "mongoose": "~3.8.1", "domain-middleware": "~0.1.0", - "swagger-node-express": "~1.3.2" + "swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg" }, "private": true, "subdomain": "habitrpg", diff --git a/src/apidoc.coffee b/src/apidoc.coffee deleted file mode 100644 index 46c9e99fd6..0000000000 --- a/src/apidoc.coffee +++ /dev/null @@ -1,329 +0,0 @@ -# see https://github.com/wordnik/swagger-node-express - -_ = require('lodash') -nconf = require('nconf') -content = require('habitrpg-shared').content - -module.exports = (swagger) -> - [path,body,query] = [swagger.pathParam, swagger.bodyParam, swagger.queryParam] - - swagger.configureSwaggerPaths("", "/api-docs", "") - - api = - "/content": - description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon)." - method: 'GET' - - "/export/history": - description: "Export user history" - method: 'GET' - - # --------------------------------- - # User - # --------------------------------- - - # Scoring - - "/user/tasks/{id}/{direction}": - description: "Simple scoring of a task" - params: [ - path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") - path("direction", "Either 'up' or 'down'", "string") - ] - method: 'POST' - - # Tasks - "/user/tasks": - description: "Get all user's tasks" - - "/user/tasks/{id}": - description: "Get an individual task" - params: [ - path("id", "Task ID", "string") - ] - - "/user/tasks/{id}": - description: "Update a user's task" - method: 'PUT' - params: [ - path("id", "Task ID", "string") - body("","Send up the whole task","object") - ] - - "/user/tasks/{id}": - description: "Delete a task" - method: 'DELETE' - params: [ path("id", "Task ID", "string") ] - - "/user/tasks": - description: "Create a task" - method: 'POST' - params: [ body("","Send up the whole task","object") ] - - "/user/tasks/{id}/sort": - method: 'POST' - description: 'Sort tasks' - params: [ - path("id", "Task ID", "string") - query("from","Index where you're sorting from (0-based)","integer") - query("to","Index where you're sorting to (0-based)","integer") - ] - - "/user/tasks/clear-completed": - method: 'POST' - description: "Clears competed To-Dos (needed periodically for performance." - - "/user/tasks/{id}/unlink": - method: 'POST' - description: 'Unlink a task from its challenge' - # TODO query params? - params: [path("id", "Task ID", "string")] - - - # Inventory - "/user/inventory/buy/{key}": - method: 'POST' - description: "Buy a gear piece and equip it automatically" - params:[ - path 'key',"The key of the item to buy (call /content route for available keys)",'string', _.keys(content.gear.flat) - #TODO embed keys - ] - - "/user/inventory/sell/{type}/{key}": - method: 'POST' - description: "Sell inventory items back to Alexander" - params: [ - #TODO verify these are the correct types - path('type',"The type of object you're selling back.",'string',['gear','eggs','hatchingPotions','food']) - path('key',"The object key you're selling back (call /content route for available keys)",'string') - ] - - "/user/inventory/purchase/{type}/{key}": - method: 'POST' - description: "Purchase a gem-purchaseable item from Alexander" - params:[ - path('type',"The type of object you're purchasing.",'string',['gear','eggs','hatchingPotions','food']) - path('key',"The object key you're purchasing (call /content route for available keys)",'string') - ] - - "/user/inventory/feed/{pet}/{food}": - method: 'POST' - description: "Feed your pet some food" - params: [ - path 'pet',"The key of the pet you're feeding",'string'#,_.keys(content.pets)) - path 'food',"The key of the food to feed your pet",'string',_.keys(content.food) - ] - - "/user/inventory/equip/{type}/{key}": - method: 'POST' - description: "Equip an item (either pets, mounts, or gear)" - params: [ - path 'type',"Type to equip",'string',['pets','mounts','gear'] - path 'key',"The object key you're equipping (call /content route for available keys)",'string' - ] - - "/user/inventory/hatch/{egg}/{hatchingPotion}": - method: 'POST' - description: "Pour a hatching potion on an egg" - params: [ - path 'egg',"The egg key to hatch",'string',_.keys(content.eggs) - path 'hatchingPotion',"The hatching potion to pour",'string',_.keys(content.hatchingPotions) - ] - - # User - "/user:GET": - path: '/user' - description: "Get the full user object" - - "/user:PUT": - path: '/user' - method: 'PUT' - description: "Update the user object (only certain attributes are supported)" - params: [ - body '','The user object','object' - ] - - "/user:DELETE": - path: '/user' - method: 'DELETE' - description: "Delete a user object entirely, USE WITH CAUTION!" - - "/user/revive": - method: 'POST' - description: "Revive your dead user" - - "/user/reroll": - method: 'POST' - description: 'Drink the Fortify Potion (Note, it used to be called re-roll)' - - "/user/reset": - method: 'POST' - description: "Completely reset your account" - - "/user/sleep": - method: 'POST' - description: "Toggle whether you're resting in the inn" - - "/user/rebirth": - method: 'POST' - description: "Rebirth your avatar" - - "/user/class/change": - method: 'POST' - description: "Either remove your avatar's class, or change it to something new" - params: [ - query 'class',"The key of the class to change to. If not provided, user's class is removed.",'string',['warrior','healer','rogue','wizard',''] - ] - - "/user/class/allocate": - method: 'POST' - description: "Allocate one point towards an attribute" - params: [ - query 'stat','The stat to allocate towards','string' - ] - - "/user/class/cast/{spell}": - method: 'POST' - description: "Cast a spell" - #TODO finish - - "/user/unlock": - method: 'POST' - description: "Unlock a certain gem-purchaseable path (or multiple paths)" - params: [ - query 'path',"The path to unlock, such as hair.green or shirts.red,shirts.blue",'string' - ] - - "/user/batch-update": - method: 'POST' - description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',params:{},body:{},query:{}}" - params:[ - body '','The array of batch-operations to perform','object' - ] - - # Tags - "/user/tags": - method: 'POST' - description: 'Create a new tag' - params: [ - #TODO document - body '','New tag','object' - ] - - "/user/tags/{id}:PUT": - path: 'user/tags/{id}' - method: 'PUT' - description: "Edit a tag" - params: [ - path 'id','The id of the tag to edit','string' - body '','Tag edits','object' - ] - - "/user/tags/{id}:DELETE": - path: 'user/tags/{id}' - method: 'DELETE' - description: 'Delete a tag' - params: [ - path 'id','Id of tag to delete','string' - ] - - - # --------------------------------- - # Groups - # --------------------------------- - "/groups:GET": - path: '/groups' - "/groups:POST": - path: '/groups' - method: 'POST' - "/groups/{gid}:GET": - path: '/groups/{gid}' - "/groups/{gid}:POST": - path: '/groups/{gid}' - method: 'POST' - "/groups/{gid}": - path: '/groups/{gid}' - method: 'PUT' - - "/groups/{gid}/join": - method: 'POST' - "/groups/{gid}/leave": - method: 'POST' - "/groups/{gid}/invite": - method: 'POST' - "/groups/{gid}/removeMember": - method: 'POST' - "/groups/{gid}/questAccept": - method: 'POST' - # query={key} (optional. if provided, trigger new invite, if not, accept existing invite) - "/groups/{gid}/questReject": - method: 'POST' - "/groups/{gid}/questAbort": - method: 'POST' - - #GET /groups/:gid/chat - "/groups/{gid}/chat": - method: 'POST' - "/groups/{gid}/chat/{messageId}": - method: 'DELETE' - - - # --------------------------------- - # Members - # --------------------------------- - "/members/{uid}":{} - - # --------------------------------- - # Challenges - # --------------------------------- - - # Note: while challenges belong to groups, and would therefore make sense as a nested resource - # (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab - # without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource - "/challenges:GET": - path: '/challenges' - "/challenges:POST": - path: '/challenges' - method: 'POST' - "/challenges/{cid}:GET": {} - "/challenges/{cid}:POST": - path: '/challenges/{cid}' - method: 'POST' - "/challenges/{cid}:DELETE": - path: '/challenges/{cid}' - method: 'DELETE' - "/challenges/{cid}/close": - method: 'POST' - "/challenges/{cid}/join": - method: 'POST' - "/challenges/{cid}/leave": - method: 'POST' - "/challenges/{cid}/member/{uid}":{} - - _.each api, (spec, path) -> - ## Spec format is: - # spec: - # path: "/pet/{petId}" - # description: "Operations about pets" - # notes: "Returns a pet based on ID" - # summary: "Find pet by ID" - # method: "GET" - # params: [path("petId", "ID of pet that needs to be fetched", "string")] - # type: "Pet" - # errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")] - # nickname: "getPetById" - - spec.description ?= '' - _.defaults spec, - path: path - nickname: path - notes: spec.description - summary: spec.description - params: [] - #type: 'Pet' - errorResponses: [] - method: 'GET' - route = {spec} - swagger["add#{route.spec.method}"](route);true - - swagger.configure(nconf.get('BASE_URL'), "0.1") \ No newline at end of file diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee new file mode 100644 index 0000000000..068c9b78ec --- /dev/null +++ b/src/routes/apiv2.coffee @@ -0,0 +1,525 @@ +### +---------- /api/v2 API ------------ +see https://github.com/wordnik/swagger-node-express +Every url added to router is prefaced by /api/v2 +Note: Many user-route ops exist in habitrpg-shard/script/index.coffee#user.ops, so that they can (1) be called both +client and server. +v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: +$ mocha test/user.mocha.coffee +### + +user = require("../controllers/user") +groups = require("../controllers/groups") +auth = require("../controllers/auth") +admin = require("../controllers/admin") +challenges = require("../controllers/challenges") +dataexport = require("../controllers/dataexport") +nconf = require("nconf") +middleware = require("../middleware") +cron = user.cron +_ = require('lodash') +content = require('habitrpg-shared').content + + +module.exports = (swagger, v2, errorHandler) -> + [path,body,query] = [swagger.pathParam, swagger.bodyParam, swagger.queryParam] + + swagger.setAppHandler(v2); + swagger.setErrorHandler("next"); + swagger.configureSwaggerPaths("", "/api-docs", "") + + api = + + '/status': + spec: + description: "Returns the status of the server (up or down)" + action: (req, res) -> + test() + res.json status: "up" + + '/content': + spec: + description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon)." + action: user.getContent + + + "/export/history": + spec: + description: "Export user history" + method: 'GET' + middleware: auth.auth + action: dataexport.history #[todo] encode data output options in the data controller and use these to build routes + + # --------------------------------- + # User + # --------------------------------- + + # Scoring + + "/user/tasks/{id}/{direction}": + spec: + description: "Simple scoring of a task" + params: [ + path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") + path("direction", "Either 'up' or 'down'", "string") + ] + method: 'POST' + action: user.score + + # Tasks + "/user/tasks": + spec: + description: "Get all user's tasks" + action: user.getTasks + + "/user/tasks/{id}": + spec: + description: "Get an individual task" + params: [ + path("id", "Task ID", "string") + ] + action: user.getTask + + "/user/tasks/{id}": + spec: + description: "Update a user's task" + method: 'PUT' + params: [ + path("id", "Task ID", "string") + body("","Send up the whole task","object") + ] + action: user.updateTask + + "/user/tasks/{id}": + spec: + description: "Delete a task" + method: 'DELETE' + params: [ path("id", "Task ID", "string") ] + action: user.deleteTask + + "/user/tasks": + spec: + description: "Create a task" + method: 'POST' + params: [ body("","Send up the whole task","object") ] + action: user.addTask + + + "/user/tasks/{id}/sort": + spec: + method: 'POST' + description: 'Sort tasks' + params: [ + path("id", "Task ID", "string") + query("from","Index where you're sorting from (0-based)","integer") + query("to","Index where you're sorting to (0-based)","integer") + ] + action: user.sortTask + + "/user/tasks/clear-completed": + spec: + method: 'POST' + description: "Clears competed To-Dos (needed periodically for performance." + action: user.clearCompleted + + + "/user/tasks/{id}/unlink": + spec: + method: 'POST' + description: 'Unlink a task from its challenge' + # TODO query params? + params: [path("id", "Task ID", "string")] + middleware: auth.auth ## removing cron since they may want to remove task first + action: challenges.unlink + + + # Inventory + "/user/inventory/buy/{key}": + spec: + method: 'POST' + description: "Buy a gear piece and equip it automatically" + params:[ + path 'key',"The key of the item to buy (call /content route for available keys)",'string', _.keys(content.gear.flat) + #TODO embed keys + ] + action: user.buy + + "/user/inventory/sell/{type}/{key}": + spec: + method: 'POST' + description: "Sell inventory items back to Alexander" + params: [ + #TODO verify these are the correct types + path('type',"The type of object you're selling back.",'string',['gear','eggs','hatchingPotions','food']) + path('key',"The object key you're selling back (call /content route for available keys)",'string') + ] + action: user.sell + + "/user/inventory/purchase/{type}/{key}": + spec: + method: 'POST' + description: "Purchase a gem-purchaseable item from Alexander" + params:[ + path('type',"The type of object you're purchasing.",'string',['gear','eggs','hatchingPotions','food']) + path('key',"The object key you're purchasing (call /content route for available keys)",'string') + ] + action: user.purchase + + + "/user/inventory/feed/{pet}/{food}": + spec: + method: 'POST' + description: "Feed your pet some food" + params: [ + path 'pet',"The key of the pet you're feeding",'string'#,_.keys(content.pets)) + path 'food',"The key of the food to feed your pet",'string',_.keys(content.food) + ] + action: user.feed + + "/user/inventory/equip/{type}/{key}": + spec: + method: 'POST' + description: "Equip an item (either pets, mounts, or gear)" + params: [ + path 'type',"Type to equip",'string',['pets','mounts','gear'] + path 'key',"The object key you're equipping (call /content route for available keys)",'string' + ] + action: user.equip + + "/user/inventory/hatch/{egg}/{hatchingPotion}": + spec: + method: 'POST' + description: "Pour a hatching potion on an egg" + params: [ + path 'egg',"The egg key to hatch",'string',_.keys(content.eggs) + path 'hatchingPotion',"The hatching potion to pour",'string',_.keys(content.hatchingPotions) + ] + action: user.hatch + + + # User + "/user:GET": + spec: + path: '/user' + description: "Get the full user object" + action: user.getUser + + "/user:PUT": + spec: + path: '/user' + method: 'PUT' + description: "Update the user object (only certain attributes are supported)" + params: [ + body '','The user object','object' + ] + action: user.update + + "/user:DELETE": + spec: + path: '/user' + method: 'DELETE' + description: "Delete a user object entirely, USE WITH CAUTION!" + middleware: auth.auth + action: user["delete"] + + "/user/revive": + spec: + method: 'POST' + description: "Revive your dead user" + action: user.revive + + "/user/reroll": + spec: + method: 'POST' + description: 'Drink the Fortify Potion (Note, it used to be called re-roll)' + action: user.reroll + + "/user/reset": + spec: + method: 'POST' + description: "Completely reset your account" + action: user.reset + + "/user/sleep": + spec: + method: 'POST' + description: "Toggle whether you're resting in the inn" + action: user.sleep + + "/user/rebirth": + spec: + method: 'POST' + description: "Rebirth your avatar" + action: user.rebirth + + "/user/class/change": + spec: + method: 'POST' + description: "Either remove your avatar's class, or change it to something new" + params: [ + query 'class',"The key of the class to change to. If not provided, user's class is removed.",'string',['warrior','healer','rogue','wizard',''] + ] + action: user.changeClass + + "/user/class/allocate": + spec: + method: 'POST' + description: "Allocate one point towards an attribute" + params: [ + query 'stat','The stat to allocate towards','string' + ] + action:user.allocate + + "/user/class/cast/{spell}": + spec: + method: 'POST' + description: "Cast a spell" + #TODO finish + action: user.cast + + "/user/unlock": + spec: + method: 'POST' + description: "Unlock a certain gem-purchaseable path (or multiple paths)" + params: [ + query 'path',"The path to unlock, such as hair.green or shirts.red,shirts.blue",'string' + ] + action: user.unlock + + "/user/buy-gems": + spec: method: 'POST', description: "Do not use this route!" + middleware: auth.auth + action:user.buyGems + + "/user/buy-gems/paypal-ipn": + spec: method: 'POST', description: "Don't use this route!" + action: user.buyGemsPaypalIPN + + "/user/batch-update": + spec: + method: 'POST' + description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',params:{},body:{},query:{}}" + params:[ + body '','The array of batch-operations to perform','object' + ] + middleware: [middleware.forceRefresh, auth.auth, cron] + action: user.batchUpdate + + # Tags + "/user/tags": + spec: + method: 'POST' + description: 'Create a new tag' + params: [ + #TODO document + body '','New tag','object' + ] + action: user.addTag + + "/user/tags/{id}:PUT": + spec: + path: 'user/tags/{id}' + method: 'PUT' + description: "Edit a tag" + params: [ + path 'id','The id of the tag to edit','string' + body '','Tag edits','object' + ] + action: user.updateTag + + "/user/tags/{id}:DELETE": + spec: + path: 'user/tags/{id}' + method: 'DELETE' + description: 'Delete a tag' + params: [ + path 'id','Id of tag to delete','string' + ] + action: user.deleteTag + + # --------------------------------- + # Groups + # --------------------------------- + "/groups:GET": + spec: path: '/groups' + middleware: auth.auth + action: groups.list + + "/groups:POST": + spec: path: '/groups', method: 'POST' + middleware: auth.auth + action: groups.create + + "/groups/{gid}:GET": + spec: path: '/groups/{gid}' + middleware: auth.auth + action: groups.get + + "/groups/{gid}": + spec: path: '/groups/{gid}', method: 'PUT' + middleware: [auth.auth, groups.attachGroup] + action: groups.update + + "/groups/{gid}/join": + spec: method: 'POST' + middleware: [auth.auth, groups.attachGroup] + action: groups.join + + "/groups/{gid}/leave": + spec: method: 'POST' + middleware: [auth.auth, groups.attachGroup] + action: groups.leave + + "/groups/{gid}/invite": + spec: method: 'POST' + middleware: [auth.auth, groups.attachGroup] + action:groups.invite + + "/groups/{gid}/removeMember": + spec:method: 'POST' + middleware: [auth.auth, groups.attachGroup] + action:groups.removeMember + + "/groups/{gid}/questAccept": + spec: + method: 'POST' + params: [ + query 'key',"optional. if provided, trigger new invite, if not, accept existing invite",'string' + ] + middleware: [auth.auth, groups.attachGroup] + action:groups.questAccept + + "/groups/{gid}/questReject": + spec: method: 'POST' + middleware: [auth.auth, groups.attachGroup] + action: groups.questReject + + "/groups/{gid}/questAbort": + spec: method: 'POST' + middleware: [auth.auth, groups.attachGroup] + action: groups.questAbort + + #TODO GET /groups/:gid/chat + #TODO PUT /groups/:gid/chat/:messageId + + "/groups/{gid}/chat": + spec: method: 'POST' + middleware: [auth.auth, groups.attachGroup] + action: groups.postChat + + "/groups/{gid}/chat/{messageId}": + spec: method: 'DELETE' + middleware: [auth.auth, groups.attachGroup] + action: groups.deleteChatMessage + + # --------------------------------- + # Members + # --------------------------------- + "/members/{uid}": + spec:{} + action: groups.getMember + + # --------------------------------- + # Admin + # --------------------------------- + "/admin/members": + spec: {} + middleware:[auth.auth, admin.ensureAdmin] + action: admin.listMembers + + "/admin/members/{uid}": + spec: {} + middleware: [auth.auth, admin.ensureAdmin] + action: admin.getMember + + "/admin/members/{uid}": + spec: method: 'POST' + middleware: [auth.auth, admin.ensureAdmin] + action: admin.updateMember + + + # --------------------------------- + # Challenges + # --------------------------------- + + # Note: while challenges belong to groups, and would therefore make sense as a nested resource + # (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab + # without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource + "/challenges:GET": + spec: path: '/challenges' + middleware: [auth.auth] + action: challenges.list + + "/challenges:POST": + spec: path: '/challenges', method: 'POST' + middleware: [auth.auth] + action: challenges.create + + "/challenges/{cid}:GET": + spec: {} + action: challenges.get + + "/challenges/{cid}:POST": + spec: path: '/challenges/{cid}', method: 'POST' + middleware: [auth.auth] + action: challenges.update + + "/challenges/{cid}:DELETE": + spec: path: '/challenges/{cid}', method: 'DELETE' + middleware: [auth.auth] + action: challenges["delete"] + + "/challenges/{cid}/close": + spec: method: 'POST' + middleware: [auth.auth] + action: challenges.selectWinner + + "/challenges/{cid}/join": + spec: method: 'POST' + middleware: [auth.auth] + action: challenges.join + + "/challenges/{cid}/leave": + spec: method: 'POST' + middleware: [auth.auth] + action: challenges.leave + + "/challenges/{cid}/member/{uid}": + spec: {} + middleware: [auth.auth] + action: challenges.getMember + + + if nconf.get("NODE_ENV") is "development" + api["/user/addTenGems"] = + spec: method:'POST' + action: user.addTenGems + + _.each api, (route, path) -> + ## Spec format is: + # spec: + # path: "/pet/{petId}" + # description: "Operations about pets" + # notes: "Returns a pet based on ID" + # summary: "Find pet by ID" + # method: "GET" + # params: [path("petId", "ID of pet that needs to be fetched", "string")] + # type: "Pet" + # errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")] + # nickname: "getPetById" + + route.spec.description ?= '' + _.defaults route.spec, + path: path + nickname: path + notes: route.spec.description + summary: route.spec.description + params: [] + #type: 'Pet' + errorResponses: [] + method: 'GET' + middleware: if path.indexOf('/user') is 0 then [auth.auth, cron] else [] + swagger["add#{route.spec.method}"](route);true + + + swagger.configure(nconf.get('BASE_URL'), "2") \ No newline at end of file diff --git a/src/routes/apiv2.js b/src/routes/apiv2.js deleted file mode 100644 index 065d152c8c..0000000000 --- a/src/routes/apiv2.js +++ /dev/null @@ -1,140 +0,0 @@ -/*** - * ---------- /api/v2 API ------------ - * Every url added to router is prefaced by /api/v2 - * Note: Many user-route ops exist in habitrpg-shard/script/index.coffee#user.ops, so that they can (1) be called both - * client and server. - * v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: - * $ mocha test/user.mocha.coffee - */ - -var express = require('express'); -var router = new express.Router(); -var user = require('../controllers/user'); -var groups = require('../controllers/groups'); -var auth = require('../controllers/auth'); -var admin = require('../controllers/admin'); -var challenges = require('../controllers/challenges'); -var dataexport = require('../controllers/dataexport'); -var nconf = require('nconf'); -var middleware = require('../middleware'); - -var cron = user.cron; - -router.get('/status', function(req, res) { - return res.json({ - status: 'up' - }); -}); - -// --------------------------------- -// User -// --------------------------------- - -// List all keys and objects in content.coffee that clients ned to know about -router.get('/content', user.getContent); - -// Data Export -router.get('/export/history',auth.auth,dataexport.history); //[todo] encode data output options in the data controller and use these to build routes - -// Scoring -router.post('/user/tasks/:id/:direction', auth.auth, cron, user.score); - -// Tasks -router.get('/user/tasks', auth.auth, cron, user.getTasks); -router.get('/user/tasks/:id', auth.auth, cron, user.getTask); -router.put('/user/tasks/:id', auth.auth, cron, user.updateTask); //Shared.ops | body={} -router["delete"]('/user/tasks/:id', auth.auth, cron, user.deleteTask); //Shared.ops -router.post('/user/tasks', auth.auth, cron, user.addTask); //Shared.ops | body={} -router.post('/user/tasks/:id/sort', auth.auth, cron, user.sortTask); //Shared.ops | query={to,from} -router.post('/user/tasks/clear-completed', auth.auth, cron, user.clearCompleted); //Shared.ops -router.post('/user/tasks/:id/unlink', auth.auth, challenges.unlink); // removing cron since they may want to remove task first - -// Inventory -router.post('/user/inventory/buy/:key', auth.auth, cron, user.buy); // Shared.ops -router.post('/user/inventory/sell/:type/:key', auth.auth, cron, user.sell); // Shared.ops -router.post('/user/inventory/purchase/:type/:key', auth.auth, user.purchase); //Shared.ops -router.post('/user/inventory/feed/:pet/:food', auth.auth, user.feed); //Shared.ops -router.post('/user/inventory/equip/:type/:key', auth.auth, user.equip); //Shared.ops -router.post('/user/inventory/hatch/:egg/:hatchingPotion', auth.auth, user.hatch); //Shared.ops - -// User -router.get('/user', auth.auth, cron, user.getUser); -router.put('/user', auth.auth, cron, user.update); // body={} -router['delete']('/user', auth.auth, user['delete']); - -router.post('/user/revive', auth.auth, cron, user.revive); // Shared.ops -router.post('/user/reroll', auth.auth, cron, user.reroll); // Shared.ops -router.post('/user/reset', auth.auth, user.reset); // Shared.ops -router.post('/user/sleep', auth.auth, cron, user.sleep); //Shared.ops -router.post('/user/rebirth', auth.auth, cron, user.rebirth); //Shared.ops - -router.post('/user/class/change', auth.auth, cron, user.changeClass); //Shared.ops | query={class} -router.post('/user/class/allocate', auth.auth, cron, user.allocate); //Shared.ops | query={stat} -router.post('/user/class/cast/:spell', auth.auth, user.cast); - -router.post('/user/unlock', auth.auth, cron, user.unlock); // Shared.ops -router.post('/user/buy-gems', auth.auth, user.buyGems); -router.post('/user/buy-gems/paypal-ipn', user.buyGemsPaypalIPN); - -router.post('/user/batch-update', middleware.forceRefresh, auth.auth, cron, user.batchUpdate); - -if (nconf.get('NODE_ENV') == 'development') router.post('/user/addTenGems', auth.auth, user.addTenGems); - -// Tags -router.post('/user/tags', auth.auth, user.addTag); //Shared.ops | body={} -router.put('/user/tags/:id', auth.auth, user.updateTag); //Shared.ops | body={} -router['delete']('/user/tags/:id', auth.auth, user.deleteTag); //Shared.ops | body={} - -// --------------------------------- -// Groups -// --------------------------------- -router.get('/groups', auth.auth, groups.list); -router.post('/groups', auth.auth, groups.create); -router.get('/groups/:gid', auth.auth, groups.get); -router.post('/groups/:gid', auth.auth, groups.attachGroup, groups.update); -router.put('/groups/:gid', auth.auth, groups.attachGroup, groups.update); -//DELETE /groups/:gid - -router.post('/groups/:gid/join', auth.auth, groups.attachGroup, groups.join); -router.post('/groups/:gid/leave', auth.auth, groups.attachGroup, groups.leave); -router.post('/groups/:gid/invite', auth.auth, groups.attachGroup, groups.invite); -router.post('/groups/:gid/removeMember', auth.auth, groups.attachGroup, groups.removeMember); -router.post('/groups/:gid/questAccept', auth.auth, groups.attachGroup, groups.questAccept); // query={key} (optional. if provided, trigger new invite, if not, accept existing invite) -router.post('/groups/:gid/questReject', auth.auth, groups.attachGroup, groups.questReject); -router.post('/groups/:gid/questAbort', auth.auth, groups.attachGroup, groups.questAbort); - -//GET /groups/:gid/chat -router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat); -router["delete"]('/groups/:gid/chat/:messageId', auth.auth, groups.attachGroup, groups.deleteChatMessage); -//PUT /groups/:gid/chat/:messageId - -// --------------------------------- -// Members -// --------------------------------- -router.get('/members/:uid', groups.getMember); - -// --------------------------------- -// Admin -// --------------------------------- -router.get('/admin/members', auth.auth, admin.ensureAdmin, admin.listMembers); -router.get('/admin/members/:uid', auth.auth, admin.ensureAdmin, admin.getMember); -router.post('/admin/members/:uid', auth.auth, admin.ensureAdmin, admin.updateMember); - -// --------------------------------- -// Challenges -// --------------------------------- - -// Note: while challenges belong to groups, and would therefore make sense as a nested resource -// (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab -// without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource -router.get('/challenges', auth.auth, challenges.list) -router.post('/challenges', auth.auth, challenges.create) -router.get('/challenges/:cid', auth.auth, challenges.get) -router.post('/challenges/:cid', auth.auth, challenges.update) -router['delete']('/challenges/:cid', auth.auth, challenges['delete']) -router.post('/challenges/:cid/close', auth.auth, challenges.selectWinner) -router.post('/challenges/:cid/join', auth.auth, challenges.join) -router.post('/challenges/:cid/leave', auth.auth, challenges.leave) -router.get('/challenges/:cid/member/:uid', auth.auth, challenges.getMember) - -module.exports = router; \ No newline at end of file diff --git a/src/server.js b/src/server.js index 3d5ad385a2..ecf1ef9ec2 100644 --- a/src/server.js +++ b/src/server.js @@ -105,14 +105,14 @@ app.use(express['static'](path.join(__dirname, "/../public"))); // Custom Directives app.use(require('./routes/pages').middleware); app.use(require('./routes/auth').middleware); -app.use('/api/v2', require('./routes/apiv2').middleware); +var v2 = express(); +app.use('/api/v2', v2); app.use('/api/v1', require('./routes/apiv1').middleware); app.use('/export', require('./routes/dataexport').middleware); app.use(utils.errorHandler); -swagger.setAppHandler(app); -require('./apidoc.coffee')(swagger); +require('./routes/apiv2.coffee')(swagger, v2); server = http.createServer(app).listen(app.get("port"), function() { return console.log("Express server listening on port " + app.get("port")); diff --git a/views/static/api.jade b/views/static/api.jade index aa89f44b51..e833694f4c 100644 --- a/views/static/api.jade +++ b/views/static/api.jade @@ -19,7 +19,7 @@ html script(type='text/javascript') $(function () { window.swaggerUi = new SwaggerUi({ - url: "/api-docs", + url: "/api/v2/api-docs", dom_id: "swagger-ui-container", supportedSubmitMethods: ['get', 'post', 'put', 'delete'], onComplete: function(swaggerApi, swaggerUi){ From 0d584bef3a6482ddfcef862a0dcbefaa856db824 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 1 Jan 2014 20:34:31 -0700 Subject: [PATCH 6/7] #2217 swagger, params => parameters. Skip swagger set headers, we have our own --- src/routes/apiv2.coffee | 60 ++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index 068c9b78ec..a6f16fd68e 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -21,11 +21,12 @@ _ = require('lodash') content = require('habitrpg-shared').content -module.exports = (swagger, v2, errorHandler) -> +module.exports = (swagger, v2) -> [path,body,query] = [swagger.pathParam, swagger.bodyParam, swagger.queryParam] - swagger.setAppHandler(v2); - swagger.setErrorHandler("next"); + swagger.setAppHandler(v2) + swagger.setErrorHandler("next") + swagger.setHeaders = -> #disable setHeaders, since we have our own thing going on in middleware.js (and which requires `req`, which swagger doesn't pass in) swagger.configureSwaggerPaths("", "/api-docs", "") api = @@ -34,7 +35,6 @@ module.exports = (swagger, v2, errorHandler) -> spec: description: "Returns the status of the server (up or down)" action: (req, res) -> - test() res.json status: "up" '/content': @@ -59,7 +59,7 @@ module.exports = (swagger, v2, errorHandler) -> "/user/tasks/{id}/{direction}": spec: description: "Simple scoring of a task" - params: [ + parameters: [ path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") path("direction", "Either 'up' or 'down'", "string") ] @@ -75,7 +75,7 @@ module.exports = (swagger, v2, errorHandler) -> "/user/tasks/{id}": spec: description: "Get an individual task" - params: [ + parameters: [ path("id", "Task ID", "string") ] action: user.getTask @@ -84,7 +84,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: description: "Update a user's task" method: 'PUT' - params: [ + parameters: [ path("id", "Task ID", "string") body("","Send up the whole task","object") ] @@ -94,14 +94,14 @@ module.exports = (swagger, v2, errorHandler) -> spec: description: "Delete a task" method: 'DELETE' - params: [ path("id", "Task ID", "string") ] + parameters: [ path("id", "Task ID", "string") ] action: user.deleteTask "/user/tasks": spec: description: "Create a task" method: 'POST' - params: [ body("","Send up the whole task","object") ] + parameters: [ body("","Send up the whole task","object") ] action: user.addTask @@ -109,7 +109,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: 'Sort tasks' - params: [ + parameters: [ path("id", "Task ID", "string") query("from","Index where you're sorting from (0-based)","integer") query("to","Index where you're sorting to (0-based)","integer") @@ -128,7 +128,7 @@ module.exports = (swagger, v2, errorHandler) -> method: 'POST' description: 'Unlink a task from its challenge' # TODO query params? - params: [path("id", "Task ID", "string")] + parameters: [path("id", "Task ID", "string")] middleware: auth.auth ## removing cron since they may want to remove task first action: challenges.unlink @@ -138,7 +138,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Buy a gear piece and equip it automatically" - params:[ + parameters:[ path 'key',"The key of the item to buy (call /content route for available keys)",'string', _.keys(content.gear.flat) #TODO embed keys ] @@ -148,7 +148,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Sell inventory items back to Alexander" - params: [ + parameters: [ #TODO verify these are the correct types path('type',"The type of object you're selling back.",'string',['gear','eggs','hatchingPotions','food']) path('key',"The object key you're selling back (call /content route for available keys)",'string') @@ -159,7 +159,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Purchase a gem-purchaseable item from Alexander" - params:[ + parameters:[ path('type',"The type of object you're purchasing.",'string',['gear','eggs','hatchingPotions','food']) path('key',"The object key you're purchasing (call /content route for available keys)",'string') ] @@ -170,7 +170,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Feed your pet some food" - params: [ + parameters: [ path 'pet',"The key of the pet you're feeding",'string'#,_.keys(content.pets)) path 'food',"The key of the food to feed your pet",'string',_.keys(content.food) ] @@ -180,7 +180,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Equip an item (either pets, mounts, or gear)" - params: [ + parameters: [ path 'type',"Type to equip",'string',['pets','mounts','gear'] path 'key',"The object key you're equipping (call /content route for available keys)",'string' ] @@ -190,7 +190,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Pour a hatching potion on an egg" - params: [ + parameters: [ path 'egg',"The egg key to hatch",'string',_.keys(content.eggs) path 'hatchingPotion',"The hatching potion to pour",'string',_.keys(content.hatchingPotions) ] @@ -209,7 +209,7 @@ module.exports = (swagger, v2, errorHandler) -> path: '/user' method: 'PUT' description: "Update the user object (only certain attributes are supported)" - params: [ + parameters: [ body '','The user object','object' ] action: user.update @@ -256,7 +256,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Either remove your avatar's class, or change it to something new" - params: [ + parameters: [ query 'class',"The key of the class to change to. If not provided, user's class is removed.",'string',['warrior','healer','rogue','wizard',''] ] action: user.changeClass @@ -265,7 +265,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Allocate one point towards an attribute" - params: [ + parameters: [ query 'stat','The stat to allocate towards','string' ] action:user.allocate @@ -281,7 +281,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: "Unlock a certain gem-purchaseable path (or multiple paths)" - params: [ + parameters: [ query 'path',"The path to unlock, such as hair.green or shirts.red,shirts.blue",'string' ] action: user.unlock @@ -298,8 +298,8 @@ module.exports = (swagger, v2, errorHandler) -> "/user/batch-update": spec: method: 'POST' - description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',params:{},body:{},query:{}}" - params:[ + description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',parameters:{},body:{},query:{}}" + parameters:[ body '','The array of batch-operations to perform','object' ] middleware: [middleware.forceRefresh, auth.auth, cron] @@ -310,7 +310,7 @@ module.exports = (swagger, v2, errorHandler) -> spec: method: 'POST' description: 'Create a new tag' - params: [ + parameters: [ #TODO document body '','New tag','object' ] @@ -321,7 +321,7 @@ module.exports = (swagger, v2, errorHandler) -> path: 'user/tags/{id}' method: 'PUT' description: "Edit a tag" - params: [ + parameters: [ path 'id','The id of the tag to edit','string' body '','Tag edits','object' ] @@ -332,7 +332,7 @@ module.exports = (swagger, v2, errorHandler) -> path: 'user/tags/{id}' method: 'DELETE' description: 'Delete a tag' - params: [ + parameters: [ path 'id','Id of tag to delete','string' ] action: user.deleteTag @@ -383,7 +383,7 @@ module.exports = (swagger, v2, errorHandler) -> "/groups/{gid}/questAccept": spec: method: 'POST' - params: [ + parameters: [ query 'key',"optional. if provided, trigger new invite, if not, accept existing invite",'string' ] middleware: [auth.auth, groups.attachGroup] @@ -503,7 +503,7 @@ module.exports = (swagger, v2, errorHandler) -> # notes: "Returns a pet based on ID" # summary: "Find pet by ID" # method: "GET" - # params: [path("petId", "ID of pet that needs to be fetched", "string")] + # parameters: [path("petId", "ID of pet that needs to be fetched", "string")] # type: "Pet" # errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")] # nickname: "getPetById" @@ -514,7 +514,7 @@ module.exports = (swagger, v2, errorHandler) -> nickname: path notes: route.spec.description summary: route.spec.description - params: [] + parameters: [] #type: 'Pet' errorResponses: [] method: 'GET' @@ -522,4 +522,4 @@ module.exports = (swagger, v2, errorHandler) -> swagger["add#{route.spec.method}"](route);true - swagger.configure(nconf.get('BASE_URL'), "2") \ No newline at end of file + swagger.configure("#{nconf.get('BASE_URL')}/api/v2", "2") \ No newline at end of file From 548d4f5b782e17f2888dcba76ec076a4792aa61c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 1 Jan 2014 21:09:01 -0700 Subject: [PATCH 7/7] #2217 swagger-ui: allow authentication for interacting with routes --- src/controllers/auth.js | 5 ++- src/routes/apiv2.coffee | 2 +- views/static/api.jade | 68 ++++++++++++++++++++++------------------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 462cf4ac59..dd508039ad 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -20,9 +20,8 @@ var NO_SESSION_FOUND = { err: "You must be logged in." }; */ api.auth = function(req, res, next) { - var token, uid; - uid = req.headers['x-api-user']; - token = req.headers['x-api-key']; + var uid = req.headers['x-api-user']; + var token = req.headers['x-api-key']; if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID); User.findOne({_id: uid,apiToken: token}, function(err, user) { if (err) return res.json(500, {err: err}); diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index a6f16fd68e..e33a68b4e5 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -518,7 +518,7 @@ module.exports = (swagger, v2) -> #type: 'Pet' errorResponses: [] method: 'GET' - middleware: if path.indexOf('/user') is 0 then [auth.auth, cron] else [] + route.middleware ?= if path.indexOf('/user') is 0 then [auth.auth, cron] else [] swagger["add#{route.spec.method}"](route);true diff --git a/views/static/api.jade b/views/static/api.jade index e833694f4c..df1a73fad4 100644 --- a/views/static/api.jade +++ b/views/static/api.jade @@ -19,30 +19,40 @@ html script(type='text/javascript') $(function () { window.swaggerUi = new SwaggerUi({ - url: "/api/v2/api-docs", - dom_id: "swagger-ui-container", - supportedSubmitMethods: ['get', 'post', 'put', 'delete'], - onComplete: function(swaggerApi, swaggerUi){ - if(console) { - console.log("Loaded SwaggerUI") - } - $('pre code').each(function(i, e) {hljs.highlightBlock(e)}); - }, - onFailure: function(data) { - if(console) { - console.log("Unable to Load SwaggerUI"); - console.log(data); - } - }, - docExpansion: "none" + url: "/api/v2/api-docs", + dom_id: "swagger-ui-container", + supportedSubmitMethods: ['get', 'post', 'put', 'delete'], + onComplete: function(swaggerApi, swaggerUi){ + if(console) { + console.log("Loaded SwaggerUI") + } + $('pre code').each(function(i, e) {hljs.highlightBlock(e)}); + }, + onFailure: function(data) { + if(console) { + console.log("Unable to Load SwaggerUI"); + console.log(data); + } + }, + docExpansion: "none" }); + debugger; + $('#input_apiKey').change(function() { var key = $('#input_apiKey')[0].value; - console.log("key: " + key); + console.log("apiKey: " + key); if(key && key.trim() != "") { console.log("added key " + key); - window.authorizations.add("key", new ApiKeyAuthorization("api_key", key, "query")); + window.authorizations.add("apiKey", new ApiKeyAuthorization("x-api-key", key, "header")); + } + }) + $('#input_uuid').change(function() { + var key = $('#input_uuid')[0].value; + console.log("uuid: " + key); + if(key && key.trim() != "") { + console.log("added key " + key); + window.authorizations.add("uuid", new ApiKeyAuthorization("x-api-user", key, "header")); } }) window.swaggerUi.load(); @@ -51,22 +61,16 @@ html #header .swagger-ui-wrap a#logo(href='http://swagger.wordnik.com') HabitRPG API Documentation (Under Construction) - //-form#api_selector - //- -
- -
-
- -
- .input - input#input_baseUrl(placeholder='http://example.com/api', name='baseUrl', type='text') - .input - input#input_apiKey(placeholder='api_key', name='apiKey', type='text') - .input - a#explore(href='#') Explore .swagger-ui-wrap(style='padding:50px') + form#api_selector + .input + input#input_uuid(placeholder='UUID', name='uuid', type='text') + input#input_apiKey(placeholder='API Key', name='apiKey', type='text') + //.input + input#input_baseUrl(placeholder='http://example.com/api', name='baseUrl', type='text') + //.input + a#explore(href='#') Explore p(style='color:red') This documentation is a work in progress. If you'd like to help finish it, see #2217. #message-bar.swagger-ui-wrap