From 3f596faf018dc68bf56b0d66faefda4b7fa21c20 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Sat, 6 Oct 2012 22:27:05 -0700 Subject: [PATCH 1/9] Move a section. No functional change. --- docs/client/api.js | 112 ++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/docs/client/api.js b/docs/client/api.js index 283ff58082..aa0a1aa3ed 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -371,6 +371,62 @@ Template.api.findone = { ] }; +Template.api.insert = { + id: "insert", + name: "collection.insert(doc, [callback])", + locus: "Anywhere", + descr: ["Insert a document in the collection. Returns its unique _id."], + args: [ + {name: "doc", + type: "Object", + descr: "The document to insert. Should not yet have an _id attribute."}, + {name: "callback", + type: "Function", + descr: "Optional. If present, called with an error object as the first argument and, if no error, the _id as the second."} + ] +}; + +Template.api.update = { + id: "update", + name: "collection.update(selector, modifier, [options], [callback])", + locus: "Anywhere", + descr: ["Modify one or more documents in the collection"], + args: [ + {name: "selector", + type: "Object: Mongo selector, or String", + type_link: "selectors", + descr: "Specifies which documents to modify"}, + {name: "modifier", + type: "Object: Mongo modifier", + type_link: "modifiers", + descr: "Specifies how to modify the documents"}, + {name: "callback", + type: "Function", + descr: "Optional. If present, called with an error object as its argument."} + ], + options: [ + {name: "multi", + type: "Boolean", + descr: "True to modify all matching documents; false to only modify one of the matching documents (the default)."} + ] +}; + +Template.api.remove = { + id: "remove", + name: "collection.remove(selector, [callback])", + locus: "Anywhere", + descr: ["Remove documents from the collection"], + args: [ + {name: "selector", + type: "Object: Mongo selector, or String", + type_link: "selectors", + descr: "Specifies which documents to remove"}, + {name: "callback", + type: "Function", + descr: "Optional. If present, called with an error object as its argument."} + ] +}; + Template.api.cursor_count = { id: "count", name: "cursor.count()", @@ -429,62 +485,6 @@ Template.api.cursor_observe = { ] }; -Template.api.insert = { - id: "insert", - name: "collection.insert(doc, [callback])", - locus: "Anywhere", - descr: ["Insert a document in the collection. Returns its unique _id."], - args: [ - {name: "doc", - type: "Object", - descr: "The document to insert. Should not yet have an _id attribute."}, - {name: "callback", - type: "Function", - descr: "Optional. If present, called with an error object as the first argument and, if no error, the _id as the second."} - ] -}; - -Template.api.update = { - id: "update", - name: "collection.update(selector, modifier, [options], [callback])", - locus: "Anywhere", - descr: ["Modify one or more documents in the collection"], - args: [ - {name: "selector", - type: "Object: Mongo selector, or String", - type_link: "selectors", - descr: "Specifies which documents to modify"}, - {name: "modifier", - type: "Object: Mongo modifier", - type_link: "modifiers", - descr: "Specifies how to modify the documents"}, - {name: "callback", - type: "Function", - descr: "Optional. If present, called with an error object as its argument."} - ], - options: [ - {name: "multi", - type: "Boolean", - descr: "True to modify all matching documents; false to only modify one of the matching documents (the default)."} - ] -}; - -Template.api.remove = { - id: "remove", - name: "collection.remove(selector, [callback])", - locus: "Anywhere", - descr: ["Remove documents from the collection"], - args: [ - {name: "selector", - type: "Object: Mongo selector, or String", - type_link: "selectors", - descr: "Specifies which documents to remove"}, - {name: "callback", - type: "Function", - descr: "Optional. If present, called with an error object as its argument."} - ] -}; - Template.api.selectors = { id: "selectors", name: "Mongo-style Selectors" From e2023399aa4f189b51455b8a1d765f368fcfe089 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Sat, 6 Oct 2012 22:28:23 -0700 Subject: [PATCH 2/9] First pass: a place for everything and everything in its place. We really need to make this easier. --- docs/client/api.html | 62 +++++++ docs/client/api.js | 427 +++++++++++++++++++++++++++++++++++++++++++ docs/client/docs.js | 42 ++++- 3 files changed, 529 insertions(+), 2 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 3accf566b9..385515606b 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -120,6 +120,8 @@ is the place to stop the observes. {{> api_box subscription_stop}} +{{> api_box subscription_userId}} + {{> api_box subscribe}} When you subscribe to a record set, it tells the server to send records @@ -215,6 +217,10 @@ intended to *simulate* the result of what the server's method will do, but without waiting for the round trip delay. If a stub throws an exception it will be logged to the console. +{{> api_box method_invocation_userId}} + +{{> api_box method_invocation_setUserId}} + {{> api_box method_invocation_isSimulation}} {{> api_box method_invocation_unblock}} @@ -623,6 +629,27 @@ Example: Logs.remove({}); +{{> api_box allow}} + +
+{{#dtdd "insert(userId, doc)"}} +blah blah +{{/dtdd}} + +{{#dtdd "update(userId, docs, fields, modifier)"}} +blah +{{/dtdd}} + +{{#dtdd "remove(userId, docs)"}} +blah +{{/dtdd}} + +
+ + +{{> api_box deny}} + +

Cursors

To create a cursor, use [`find`](#find). To access the documents in a @@ -1460,6 +1487,41 @@ sub-template. +

Accounts

+ +{{> api_box user}} + +{{> api_box userId}} + +{{> api_box users}} + +XXX schema? + +{{> api_box logout}} +{{> api_box loginWithPassword}} +{{> api_box loginWithFacebook}} +{{> api_box loginWithGithub}} +{{> api_box loginWithGoogle}} +{{> api_box loginWithTwitter}} +{{> api_box loginWithWeibo}} + +{{> api_box accounts_createUser}} +{{> api_box accounts_changePassword}} +{{> api_box accounts_forgotPassword}} +{{> api_box accounts_resetPassword}} +{{> api_box accounts_validateEmail}} + +{{> api_box accounts_config}} +{{> api_box accounts_emailTemplates}} +{{> api_box accounts_validateNewUser}} +{{> api_box accounts_onCreateUser}} +{{> api_box accounts_setPassword}} +{{> api_box accounts_configuration}} + +{{> api_box accounts_facebook_config}} +{{> api_box accounts_google_config}} + +

Timers

Meteor uses global environment variables diff --git a/docs/client/api.js b/docs/client/api.js index aa0a1aa3ed..a9314df0f4 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -145,6 +145,14 @@ Template.api.subscription_onStop = { ] }; +Template.api.subscription_userId = { + id: "publish_userId", + name: "this.userId()", + locus: "Server", + descr: ["Returns the id of current user, or `null` if no user logged in. The publish function is rerun when this changes."] +}; + + Template.api.subscribe = { id: "meteor_subscribe", name: "Meteor.subscribe(name [, arg1, arg2, ... ] [, onComplete])", @@ -187,6 +195,25 @@ Template.api.methods = { ] }; +Template.api.method_invocation_userId = { + id: "method_userId", + name: "this.userId()", + locus: "Anywhere", + descr: ["Returns the id of current user, or `null` if no user logged in. XXX Will remain constant."] +}; + +Template.api.method_invocation_setUserId = { + id: "method_setUserId", + name: "this.setUserId(userId)", + locus: "Server", + descr: ["Set the userId for this session. This affects all future method calls. Pass `null` to log the user out."], + args: [ + {name: "userId", + type: "String", + descr: "XXX"} + ] +}; + Template.api.method_invocation_unblock = { id: "method_unblock", name: "this.unblock()", @@ -427,6 +454,31 @@ Template.api.remove = { ] }; +Template.api.allow = { + id: "allow", + name: "collection.allow(options)", + locus: "Server", + descr: ["Control access to XXX"], + options: [ + {name: "insert", + type: "Function", + descr: "XXX"} + ] +}; + +Template.api.deny = { + id: "deny", + name: "collection.deny(options)", + locus: "Server", + descr: ["Control access to XXX"], + options: [ + {name: "insert", + type: "Function", + descr: "XXX"} + ] +}; + + Template.api.cursor_count = { id: "count", name: "cursor.count()", @@ -610,6 +662,381 @@ Template.api.isolate = { +Template.api.user = { + id: "meteor_user", + name: "Meteor.user()", + locus: "XXX client+methods", + descr: ["Returns the current user object, of null if not logged in. A reactive data source."] +}; + +Template.api.userId = { + id: "meteor_userid", + name: "Meteor.userId()", + locus: "XXX client+methods", + descr: ["Returns the current user id, of null if not logged in. A reactive data source."] +}; + + +Template.api.users = { + id: "meteor_users", + name: "Meteor.users", + locus: "Anywhere", + descr: ["A XXXlinkify Collection holding user documents."] +}; + + +Template.api.logout = { + id: "meteor_logout", + name: "Meteor.logout([callback])", + locus: "Client", + descr: ["Log the user out."], + args: [ + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + + +Template.api.loginWithPassword = { + id: "meteor_loginwithpassword", + name: "Meteor.loginWithPassword(user, password, [callback])", + locus: "Client", + descr: ["Log the user in."], + args: [ + { + name: "user", + type: "Object or String", + descr: "XXX" + }, + { + name: "password", + type: "String", + descr: "XXX" + }, + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + + +Template.api.loginWithFacebook = { + id: "meteor_loginwithfacebook", + name: "Meteor.loginWithFacebook([callback])", + locus: "Client", + descr: ["Log the user in."], + args: [ + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + +Template.api.loginWithGithub = { + id: "meteor_loginwithgithub", + name: "Meteor.loginWithGithub([callback])", + locus: "Client", + descr: ["Log the user in."], + args: [ + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + +Template.api.loginWithGoogle = { + id: "meteor_loginwithgoogle", + name: "Meteor.loginWithGoogle([callback])", + locus: "Client", + descr: ["Log the user in."], + args: [ + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + +Template.api.loginWithTwitter = { + id: "meteor_loginwithtwitter", + name: "Meteor.loginWithTwitter([callback])", + locus: "Client", + descr: ["Log the user in."], + args: [ + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + +Template.api.loginWithWeibo = { + id: "meteor_loginwithweibo", + name: "Meteor.loginWithWeibo([callback])", + locus: "Client", + descr: ["Log the user in."], + args: [ + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + + +Template.api.accounts_createUser = { + id: "accounts_createuser", + name: "Accounts.createUser(options, extra, [callback])", + locus: "Anywhere", + descr: ["Create a new user."], + args: [ + { + name: "options", + type: "Object", + descr: "XXX" + }, + { + name: "extra", + type: "Object", + descr: "XXX" + }, + { + name: "callback", + type: "Function", + descr: "XXX" + } + ], + options: [ + { + name: "username", + type: "String", + descr: "XXX" + }, + { + name: "email", + type: "String", + descr: "XXX" + }, + { + name: "password", + type: "String", + descr: "XXX" + } + ] +}; + +Template.api.accounts_changePassword = { + id: "accounts_changepassword", + name: "Accounts.changePassword(oldPassword, newPassword, [callback])", + locus: "Client", + descr: ["Change the users password. Must be logged in."], + args: [ + { + name: "oldPassword", + type: "String", + descr: "XXX" + }, + { + name: "newPassword", + type: "String", + descr: "XXX" + }, + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + +Template.api.accounts_forgotPassword = { + id: "accounts_forgotpassword", + name: "Accounts.forgotPassword(options, [callback])", + locus: "Client", + descr: ["Request a forgot password email"], + args: [ + { + name: "callback", + type: "Function", + descr: "XXX" + } + ], + options: [ + { + name: "email", + type: "String", + descr: "The email address to send a password reminder to." + } + ] +}; + +Template.api.accounts_resetPassword = { + id: "accounts_resetpassword", + name: "Accounts.resetPassword(token, newPassword, [callback])", + locus: "Client", + descr: ["Reset the password for a user, using the token from email. Logs you in afterwards."], + args: [ + { + name: "token", + type: "String", + descr: "XXX" + }, + { + name: "newPassword", + type: "String", + descr: "XXX" + }, + { + name: "callback", + type: "Function", + descr: "XXX" + } + ], + options: [ + { + name: "email", + type: "String", + descr: "The email address to send a password reminder to." + } + ] +}; + +Template.api.accounts_validateEmail = { + id: "accounts_validateemail", + name: "Accounts.validateEmail(token, [callback])", + locus: "Client", + descr: ["Marks the user's email address as validated. Logs the user in afterwards."], + args: [ + { + name: "token", + type: "String", + descr: "XXX" + }, + { + name: "callback", + type: "Function", + descr: "XXX" + } + ] +}; + + + + +Template.api.accounts_config = { + id: "accounts_config", + name: "Accounts.config(options)", + locus: "Anywhere", + descr: ["Set global accounts options."], + options: [ + { + name: "validateEmail", + type: "Boolean", + descr: "If true, new users with an email address will receive a verifcation email." + }, + { + name: "forbidSignups", + type: "Boolean", + descr: "If true, new user signups will be rejected. Server-side user creation via XXXlinkify createUser is still allowed." + } + ] +}; + +Template.api.accounts_emailTemplates = { + id: "accounts_emailtemplates", + name: "Accounts.emailTemplates", + locus: "Anywhere", + descr: ["XXX"] +}; + + +Template.api.accounts_validateNewUser = { + id: "accounts_validatenewuser", + name: "Accounts.validateNewUser(func)", + locus: "Server", + descr: ["Set up a new user validator."], + args: [ + { + name: "func", + type: "Function", + descr: "Validator function. Takes a new user object, and returns true to allow the creation or false to abort." + } + ] +}; + +Template.api.accounts_onCreateUser = { + id: "accounts_oncreateuser", + name: "Accounts.onCreateUser(func)", + locus: "Server", + descr: ["Set up a new user mangler XXX."], + args: [ + { + name: "func", + type: "Function", + descr: "Validator function. Takes `(options, extra, user)`. Return the new user, or throw an `Error`." + } + ] +}; + + +Template.api.accounts_setPassword = { + id: "accounts_setpassword", + name: "Accounts.setPassword(userId, newPassword)", + locus: "Server", + descr: ["Force change the password for a user."], + args: [ + { + name: "userId", + type: "String", + descr: "XXX" + }, + { + name: "newPassword", + type: "String", + descr: "XXX" + } + ] +}; + +Template.api.accounts_configuration = { + id: "accounts_configuration", + name: "Accounts.configuration", + locus: "Anywhere", + descr: ["A XXXlinkify Collection holding login service info."] +}; + + +Template.api.accounts_facebook_config = { + id: "accounts_facebook_config", + name: "Accounts.facebook.config(options)", + locus: "Anywhere", + descr: ["XXX REMOVE!"] +}; + +Template.api.accounts_google_config = { + id: "accounts_google_config", + name: "Accounts.google.config(options)", + locus: "Anywhere", + descr: ["XXX REMOVE!"] +}; + + + + + + Template.api.setTimeout = { id: "meteor_settimeout", name: "Meteor.setTimeout", diff --git a/docs/client/docs.js b/docs/client/docs.js index bd90697b0c..bc0b08b6b6 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -93,7 +93,8 @@ var toc = [ {instance: "this", name: "complete", id: "publish_complete"}, {instance: "this", name: "flush", id: "publish_flush"}, {instance: "this", name: "onStop", id: "publish_onstop"}, - {instance: "this", name: "stop", id: "publish_stop"} + {instance: "this", name: "stop", id: "publish_stop"}, + {instance: "this", name: "userId", id: "publish_userId"} ], "Meteor.subscribe", "Meteor.autosubscribe" @@ -101,6 +102,8 @@ var toc = [ {name: "Methods", id: "methods_header"}, [ "Meteor.methods", [ + {instance: "this", name: "userId", id: "method_userId"}, + {instance: "this", name: "setUserId", id: "method_setUserId"}, {instance: "this", name: "isSimulation", id: "method_issimulation"}, {instance: "this", name: "unblock", id: "method_unblock"} ], @@ -121,7 +124,9 @@ var toc = [ {instance: "collection", name: "findOne"}, {instance: "collection", name: "insert"}, {instance: "collection", name: "update"}, - {instance: "collection", name: "remove"} + {instance: "collection", name: "remove"}, + {instance: "collection", name: "allow"}, + {instance: "collection", name: "deny"} ], "Meteor.Collection.Cursor", [ @@ -168,6 +173,39 @@ var toc = [ {name: "Reactivity isolation", style: "noncode", id: "isolate"} ], + {name: "Accounts", id: "accounts_api"}, [ + "Meteor.user", + "Meteor.userId", + "Meteor.users", + "Meteor.logout", + "Meteor.loginWithPassword", + "Meteor.loginWithFacebook", + "Meteor.loginWithGithub", + "Meteor.loginWithGoogle", + "Meteor.loginWithTwitter", + "Meteor.loginWithWeibo", + {type: "spacer"}, + + "Accounts.createUser", + "Accounts.changePassword", + "Accounts.forgotPassword", + "Accounts.resetPassword", + "Accounts.validateEmail", + {type: "spacer"}, + + "Accounts.config", + "Accounts.emailTemplates", + "Accounts.validateNewUser", + "Accounts.onCreateUser", + "Accounts.setPassword", + "Accounts.configuration", + {type: "spacer"}, + + "Accounts.facebook.config", + "Accounts.google.config" + ], + + "Timers", [ "Meteor.setTimeout", "Meteor.setInterval", From 5097fc283c366f596c231eef7b8b7ce89e17212b Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Sun, 7 Oct 2012 19:02:20 -0700 Subject: [PATCH 3/9] Document allow/deny. --- docs/client/api.html | 138 ++++++++++++++++++++++++++++++++++++++++++- docs/client/api.js | 24 +++++--- docs/client/docs.css | 4 ++ 3 files changed, 154 insertions(+), 12 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 385515606b..ca8da6c1ab 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -203,6 +203,8 @@ object, which provides the following: * `isSimulation`: a boolean value, true if this invocation is a stub. * `unblock`: when called, allows the next method from this client to begin running. +* `userId`: a function that returns the id of the current user. +* `setUserId`: a function that associates the current client with a user. Calling `methods` on the client defines *stub* functions associated with server methods of the same name. You don't have to define a stub for @@ -217,10 +219,23 @@ intended to *simulate* the result of what the server's method will do, but without waiting for the round trip delay. If a stub throws an exception it will be logged to the console. + {{> api_box method_invocation_userId}} +The user for a method is determined when the method starts running and +will not change during method execution, even if `unblock` is called. + {{> api_box method_invocation_setUserId}} +Calling this function from a method will result in all future methods +received on the same connection having their `userId` set to the +provided value. Future calls to `this.userId` in the same method will +also return the new value. + +Values should correspond to the `_id` field of a document in the +`Meteor.users` collection. + + {{> api_box method_invocation_isSimulation}} {{> api_box method_invocation_unblock}} @@ -631,24 +646,141 @@ Example: {{> api_box allow}} +`allow` and its sibling function `deny` control +permission to write to the server database from the client using +auto-generated `insert`, `update` and `remove` methods. This lets +clients use the same API as the server to update the database while +maintaining the ability to enforce security against rogue clients. + +Writes initiated on the server, such as from a method, are always +allowed. The `allow` and `deny` functions only +control the client's access to the server database API. Using custom methods instead of `allow`ing direct database +access is a good approach to creating a secure app, but it is sometimes +easier and faster to develop with a simple access control function based +approach to security. + +Each access control function receives the `userId` of the currently +logged in user (or `null` if the user is not logged in) as the first +argument, and the document or documents in question as the second +argument. If the function returns `true`, it indicates that the write +should be allowed (in the case of `allow`) or denied (in the case of +`deny`). If the function returns false it does not allow or deny the +request. If no other `allow` statement allows the request, it is denied. + +
{{#dtdd "insert(userId, doc)"}} -blah blah +`doc` is the document the client wants to insert. {{/dtdd}} {{#dtdd "update(userId, docs, fields, modifier)"}} -blah +`docs` is an array of documents that the client is asking to modify. These +are fetched from the database and have not yet had the request modifications +applied. If you pass the `fetch` argument to `allow` and `deny`, these +documents can contain a subset of the database fields, which can be a +performance improvement. + +`fields` is a list of top level fields in the document the client is +asking to modify. If the client asks to modify a sub-field +(eg with `{$set: {'foo.bar': true}}`) this will contain only the top level +field (eg `['foo']`). + +`modifier` is the raw mongo modifier the client sent. Many uses will not +need to inspect this parameter. {{/dtdd}} {{#dtdd "remove(userId, docs)"}} -blah +`docs` is the list of documents the client is asking to remove. As with +`update` above, the `fetch` option controls which fields are populated +in `docs`. {{/dtdd}}
+Example: XXX test me! + + // Create a collection where users can only write documents that + // they 'own' using an 'owner' field in each document. + Posts = new Meteor.Collection("posts"); + + Posts.allow({ + insert: function (userId, doc) { + // the user must be logged in, and the document must be owned by the user + return (userId && doc.owner === userId); + }, + update: function (userId, docs, fields, modifier) { + // can only change your own documents + return _.all(docs, function(doc) { + return doc.owner === userId; + }); + }, + remove: function (userId, docs) { + // can only remove your own documents + return _.all(docs, function(doc) { + return doc.owner === userId; + }); + }, + fetch: ['owner'] + }); + + Posts.deny({ + update: function (userId, docs, fields, modifier) { + // can't change owners + return _.contains(fields, 'owner'); + }, + remove: function (userId, docs) { + // can't remove locked documents + return _.any(docs, function (doc) { + return doc.locked; + }); + }, + fetch: ['locked'] + }); + + + +`allow` and `deny` can be called multiple times to allow or deny + additional actions. The access control functions are evaluated as follows: + +- If any deny function returns true, the request is denied. +- Otherwise, if any allow function returns true, the request is allowed. +- Otherwise, the request is denied. + +In other words, the results of multiple functions are logically `OR`-ed +together. The execution order of multiple functions is not specified, +and execution of validators is not guaranteed. If the system can +determine the result after executing only a subset of the validators, it +will not call the remaining ones. + +The `insecure` package controls what is allowed when you do not call +`allow` or `deny` on a collection. With the `insecure` package +installed, all writes are allowed on collections that have never had +`allow` or `deny` called. This can be useful during development, to +quickly prototype differnt schemas without worring about security. Calling +`allow({})` on a collection will disable all client writes, even when +the `insecure` package is loaded. + + + {{> api_box deny}} +This is the sibling function to `allow`, it takes +the same arguments. The only difference is that when an access control +function registered with `deny` returns true, the request is denied +instead of allowed. + +Calling `deny` only has an effect when there are also calls to `allow`. If there are no `allow` +functions registered, `deny` functions are not called, as there is no +way the request would be permitted. + +`deny` functions take precedence over `allow` +functions. If any `deny` function returns true the request is denied and +the `allow` functions are not evaluated. + +

Cursors

diff --git a/docs/client/api.js b/docs/client/api.js index a9314df0f4..a2eddf5d25 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -199,18 +199,18 @@ Template.api.method_invocation_userId = { id: "method_userId", name: "this.userId()", locus: "Anywhere", - descr: ["Returns the id of current user, or `null` if no user logged in. XXX Will remain constant."] + descr: ["Returns the id of the current user, or `null` if no user is logged in."] }; Template.api.method_invocation_setUserId = { id: "method_setUserId", name: "this.setUserId(userId)", locus: "Server", - descr: ["Set the userId for this session. This affects all future method calls. Pass `null` to log the user out."], + descr: ["Set a user id for this session."], args: [ {name: "userId", type: "String", - descr: "XXX"} + descr: "The id of the user for this connection, or `null` to log the user out."} ] }; @@ -458,11 +458,14 @@ Template.api.allow = { id: "allow", name: "collection.allow(options)", locus: "Server", - descr: ["Control access to XXX"], + descr: ["Specify access control functions to allow clients to write to this collection using the default Mongo mutator methods."], options: [ - {name: "insert", + {name: "insert, update, remove", type: "Function", - descr: "XXX"} + descr: "Access control functions that are called for each client-initiated database write. Return true to allow the write. See below for details on the arguments to access control functions. You can specify any combination of the three functions."}, + {name: "fetch", + type: "Array of Strings", + descr: "Specific fields to retrieve when reading documents from the database to pass to `update` and `remove` access control functions. If this is not specified, all fields are retrieved. Specifying which fields to fetch can be a performance improvement."} ] }; @@ -470,11 +473,14 @@ Template.api.deny = { id: "deny", name: "collection.deny(options)", locus: "Server", - descr: ["Control access to XXX"], + descr: ["Specify access control functions to forbid clients from writing to this collection using the default Mongo mutator methods."], options: [ - {name: "insert", + {name: "insert, update, remove", type: "Function", - descr: "XXX"} + descr: "Access control functions that are called for each client-initiated database write. Return true to deny the write. See `allow` for details."}, + {name: "fetch", + type: "Array of Strings", + descr: "Specific fields to retrieve when reading documents from the database to pass to `update` and `remove` access control functions. If this is not specified, all fields are retrieved. Specifying which fields to fetch can be a performance improvement."} ] }; diff --git a/docs/client/docs.css b/docs/client/docs.css index 3905086782..b4661d1730 100644 --- a/docs/client/docs.css +++ b/docs/client/docs.css @@ -265,6 +265,10 @@ dl.callbacks dt .name, dl.methods dt .name { font-size: 1.1em; } +dl.callbacks { + margin-left: 1.5em; +} + #main dd p { margin-top: 0.5em; } From 17030e572c26459717b5e1033112e14b7426405a Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 8 Oct 2012 21:15:00 -0700 Subject: [PATCH 4/9] Rework allow/deny, userId, and setUserId --- docs/client/api.html | 178 ++++++++++++++++++++++++------------------- docs/client/api.js | 28 +++---- 2 files changed, 112 insertions(+), 94 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index ca8da6c1ab..d1b2964222 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -122,6 +122,9 @@ is the place to stop the observes. {{> api_box subscription_userId}} +This is constant. However, if the logged-in user changes, the publish +function is rerun with the new value. + {{> api_box subscribe}} When you subscribe to a record set, it tells the server to send records @@ -222,19 +225,25 @@ exception it will be logged to the console. {{> api_box method_invocation_userId}} -The user for a method is determined when the method starts running and -will not change during method execution, even if `unblock` is called. +The user id is an arbitrary string — typically the id of the user +record in the database. You can set it with the `setUserId` function. If +you're using the Meteor accounts system then this is handled for you. {{> api_box method_invocation_setUserId}} -Calling this function from a method will result in all future methods -received on the same connection having their `userId` set to the -provided value. Future calls to `this.userId` in the same method will -also return the new value. +Call this function to change the currently logged in user on the +connection that made this method call. This simply sets the value of +`userId` for future method calls received on this connection. Pass +`null` to log out the connection. -Values should correspond to the `_id` field of a document in the +If you are using the built-in Meteor accounts system then this should correspond to +the `_id` field of a document in the `Meteor.users` collection. +`setUserId` is not retroactive. It affects the current method call and +any future method calls on the connection. Any previous method calls on +this connection will still see the value of `userId` that was in effect +when they started. {{> api_box method_invocation_isSimulation}} @@ -646,63 +655,78 @@ Example: {{> api_box allow}} -`allow` and its sibling function `deny` control -permission to write to the server database from the client using -auto-generated `insert`, `update` and `remove` methods. This lets -clients use the same API as the server to update the database while -maintaining the ability to enforce security against rogue clients. +When a client calls `insert`, `update`, or `remove` on a collection, the +collection's `allow` and `deny` callbacks are called +on the server to determine if the write should be allowed. If at least +one `allow` callback allows the write, and no `deny` callbacks deny the +write, then the write is allowed to proceed. -Writes initiated on the server, such as from a method, are always -allowed. The `allow` and `deny` functions only -control the client's access to the server database API. Using custom methods instead of `allow`ing direct database -access is a good approach to creating a secure app, but it is sometimes -easier and faster to develop with a simple access control function based -approach to security. +These checks are run only when a client tries to write to the database +directly, for example by calling `update` from inside an event +handler. Server code is trusted and isn't subject to `allow` and `deny` +restrictions. That includes methods that are called with `Meteor.call` +— they are expected to do their own access checking rather than +relying on `allow` and `deny`. -Each access control function receives the `userId` of the currently -logged in user (or `null` if the user is not logged in) as the first -argument, and the document or documents in question as the second -argument. If the function returns `true`, it indicates that the write -should be allowed (in the case of `allow`) or denied (in the case of -`deny`). If the function returns false it does not allow or deny the -request. If no other `allow` statement allows the request, it is denied. +You can call `allow` as many times as you like, and each call can +include any combination of `insert`, `update`, and `remove` +functions. The functions should return `true` if they think the +operation should be allowed. Otherwise they should return `false`, or +nothing at all (`undefined`). In that case Meteor will continue +searching through any other `allow` rules on the collection. +The available callbacks are:
{{#dtdd "insert(userId, doc)"}} -`doc` is the document the client wants to insert. +The user `userId` wants to insert the document `doc` into the +collection. Return `true` if this should be allowed. {{/dtdd}} {{#dtdd "update(userId, docs, fields, modifier)"}} -`docs` is an array of documents that the client is asking to modify. These -are fetched from the database and have not yet had the request modifications -applied. If you pass the `fetch` argument to `allow` and `deny`, these -documents can contain a subset of the database fields, which can be a -performance improvement. +The user `userId` wants to update some documents. Meteor has fetched the +documents from the database and they are available in `docs` as an +array. Return `true` if the user should be allowed to change these +documents. -`fields` is a list of top level fields in the document the client is -asking to modify. If the client asks to modify a sub-field -(eg with `{$set: {'foo.bar': true}}`) this will contain only the top level -field (eg `['foo']`). +Additional details about the proposed modification are in `fields` and +`modifier`. `fields` is the top-level fields in the document that the +client wishes to modify, for example `['name', 'score']`. `modifier` is +the raw Mongo modifier that the client wants to execute, for example +`{$set: {'name.first': "Alice"}, $inc: {score: 1}}`. + +Only Mongo modifiers are supported (operations like `$set` and `$push`.) +If the user tries to replace the entire document rather than use +$-modifiers, the request will be denied without checking the `allow` +functions. -`modifier` is the raw mongo modifier the client sent. Many uses will not -need to inspect this parameter. {{/dtdd}} {{#dtdd "remove(userId, docs)"}} -`docs` is the list of documents the client is asking to remove. As with -`update` above, the `fetch` option controls which fields are populated -in `docs`. +The user `userId` wants to remove some documents. Meteor has fetched the +documents from the database and they are available in `docs` as an +array. Return `true` if the user should be allowed to remove these +documents. {{/dtdd}}
+By default, when Meteor fetches the documents from the database for the +`docs` array, it will retrieve all of the fields in the documents. For +efficiency you may instead want to retrieve just the fields that are +actually needed by your functions. This is enabled by the `fetch` +option. Set `fetch` to an array of the field names that should be +retrieved. Example: XXX test me! - // Create a collection where users can only write documents that - // they 'own' using an 'owner' field in each document. + // Create a collection where users can only modify documents that + // they own. Ownership is tracked by an 'owner' field on each + // document. All documents must be owned by the user that created + // them and ownership can't be changed. Only a document's owner + // is allowed to delete it, and the 'locked' attribute can be + // set on a document to prevent its accidental deletion. + Posts = new Meteor.Collection("posts"); Posts.allow({ @@ -736,51 +760,45 @@ Example: XXX test me! return doc.locked; }); }, - fetch: ['locked'] + fetch: ['locked'] // no need to fetch 'owner' }); +If you never set up any `allow` rules on a collection then all client +writes to the collection will be denied, and it will only be possible to +write to the collection from server-side code. In this case you will +have to create a method for each possible write that clients are allowed +to do. You'll then call these methods with `Meteor.call` rather than +having the clients call `insert`, `update`, and `remove` directly on the +collection. +Meteor also has a special "insecure mode" for quickly prototyping new +applications. In insecure mode, if you haven't set up any `allow` or +`deny` rules on a collection, then all users have full write access to +the collection. This is the only effect of insecure mode. If you call +`allow` or `deny` at all, even `allow({})`, then access is checked just +like normal. __New Meteor projects start in insecure mode by default.__ To +turn it off just type `meteor remove insecure`. -`allow` and `deny` can be called multiple times to allow or deny - additional actions. The access control functions are evaluated as follows: - -- If any deny function returns true, the request is denied. -- Otherwise, if any allow function returns true, the request is allowed. -- Otherwise, the request is denied. - -In other words, the results of multiple functions are logically `OR`-ed -together. The execution order of multiple functions is not specified, -and execution of validators is not guaranteed. If the system can -determine the result after executing only a subset of the validators, it -will not call the remaining ones. - -The `insecure` package controls what is allowed when you do not call -`allow` or `deny` on a collection. With the `insecure` package -installed, all writes are allowed on collections that have never had -`allow` or `deny` called. This can be useful during development, to -quickly prototype differnt schemas without worring about security. Calling -`allow({})` on a collection will disable all client writes, even when -the `insecure` package is loaded. - - +{{#note}} +For `update` and `remove`, documents will be affected only if they match +the selector both at the time the documents are fetched to run the +`allow` and `deny` rules, __and__ at the time that the operation is +actually executed. This is accomplished by rewriting the selector to +`{$and: [(original selector), {$in: {_id: [(ids of documents fetched +and checked by allow and deny)]}}]}`. +{{/note}} {{> api_box deny}} -This is the sibling function to `allow`, it takes -the same arguments. The only difference is that when an access control -function registered with `deny` returns true, the request is denied -instead of allowed. - -Calling `deny` only has an effect when there are also calls to `allow`. If there are no `allow` -functions registered, `deny` functions are not called, as there is no -way the request would be permitted. - -`deny` functions take precedence over `allow` -functions. If any `deny` function returns true the request is denied and -the `allow` functions are not evaluated. - +This works just like `allow`, except it lets you +make sure that certain writes are definitely denied, even if there is an +`allow` rule that says that they should be permitted. +When a client tries to write to a collection, the Meteor server first +checks the collection's `deny` rules. If none of them return true then +it checks the collection's `allow` rules. Meteor allows the write only +if no `deny` rules return `true` and at least one `allow` rule returns +`true`.

Cursors

diff --git a/docs/client/api.js b/docs/client/api.js index a2eddf5d25..c01c0cd4ac 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -147,9 +147,9 @@ Template.api.subscription_onStop = { Template.api.subscription_userId = { id: "publish_userId", - name: "this.userId()", + name: "this.userId", locus: "Server", - descr: ["Returns the id of current user, or `null` if no user logged in. The publish function is rerun when this changes."] + descr: ["The id of logged-in user, or `null` if no user is logged in."] }; @@ -197,20 +197,20 @@ Template.api.methods = { Template.api.method_invocation_userId = { id: "method_userId", - name: "this.userId()", + name: "this.userId", locus: "Anywhere", - descr: ["Returns the id of the current user, or `null` if no user is logged in."] + descr: ["The id of the user that made this method call, or `null` if no user was logged in."] }; Template.api.method_invocation_setUserId = { id: "method_setUserId", name: "this.setUserId(userId)", locus: "Server", - descr: ["Set a user id for this session."], + descr: ["Set the logged in user."], args: [ {name: "userId", - type: "String", - descr: "The id of the user for this connection, or `null` to log the user out."} + type: "String or null", + descr: "The value that should be returned by `userId` on this connection."} ] }; @@ -458,14 +458,14 @@ Template.api.allow = { id: "allow", name: "collection.allow(options)", locus: "Server", - descr: ["Specify access control functions to allow clients to write to this collection using the default Mongo mutator methods."], + descr: ["Allow users to write directly to this collection from client code, subject to limitations you define."], options: [ {name: "insert, update, remove", type: "Function", - descr: "Access control functions that are called for each client-initiated database write. Return true to allow the write. See below for details on the arguments to access control functions. You can specify any combination of the three functions."}, + descr: "Functions that look at a proposed modification to the database and return true if it should be allowed."}, {name: "fetch", - type: "Array of Strings", - descr: "Specific fields to retrieve when reading documents from the database to pass to `update` and `remove` access control functions. If this is not specified, all fields are retrieved. Specifying which fields to fetch can be a performance improvement."} + type: "Array of String", + descr: "Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions."} ] }; @@ -473,14 +473,14 @@ Template.api.deny = { id: "deny", name: "collection.deny(options)", locus: "Server", - descr: ["Specify access control functions to forbid clients from writing to this collection using the default Mongo mutator methods."], + descr: ["Override `allow` rules."], options: [ {name: "insert, update, remove", type: "Function", - descr: "Access control functions that are called for each client-initiated database write. Return true to deny the write. See `allow` for details."}, + descr: "Functions that look at a proposed modification to the database and return true if it should be denied, even if an `allow` rule says otherwise."}, {name: "fetch", type: "Array of Strings", - descr: "Specific fields to retrieve when reading documents from the database to pass to `update` and `remove` access control functions. If this is not specified, all fields are retrieved. Specifying which fields to fetch can be a performance improvement."} + descr: "Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions."} ] }; From 99e1ed82c7710b1c8ff383c29db66bbcf72839fd Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 9 Oct 2012 01:29:52 -0700 Subject: [PATCH 5/9] Catch up to new APIs. --- docs/client/api.html | 23 +++-- docs/client/api.js | 218 +++++++++++++++++++++++++++++-------------- docs/client/docs.js | 20 ++-- 3 files changed, 176 insertions(+), 85 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index d1b2964222..345eaea926 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1647,6 +1647,14 @@ sub-template. XXX schema? +{{> api_box userLoaded}} + +{{#note}} +We realize this is inconvenient. It is a temporary solution. In the +future we will either make it unnecessary or fold it into a more +general mechanism. +{{/note}} + {{> api_box logout}} {{> api_box loginWithPassword}} {{> api_box loginWithFacebook}} @@ -1659,17 +1667,18 @@ XXX schema? {{> api_box accounts_changePassword}} {{> api_box accounts_forgotPassword}} {{> api_box accounts_resetPassword}} -{{> api_box accounts_validateEmail}} +{{> api_box accounts_setPassword}} +{{> api_box accounts_verifyEmail}} + +{{> api_box accounts_sendResetPasswordEmail}} +{{> api_box accounts_sendVerificationEmail}} +{{> api_box accounts_sendEnrollmentEmail}} +{{> api_box accounts_emailTemplates}} {{> api_box accounts_config}} -{{> api_box accounts_emailTemplates}} +{{> api_box accounts_ui_config}} {{> api_box accounts_validateNewUser}} {{> api_box accounts_onCreateUser}} -{{> api_box accounts_setPassword}} -{{> api_box accounts_configuration}} - -{{> api_box accounts_facebook_config}} -{{> api_box accounts_google_config}}

Timers

diff --git a/docs/client/api.js b/docs/client/api.js index c01c0cd4ac..c1a268cc25 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -671,15 +671,23 @@ Template.api.isolate = { Template.api.user = { id: "meteor_user", name: "Meteor.user()", - locus: "XXX client+methods", - descr: ["Returns the current user object, of null if not logged in. A reactive data source."] + locus: "Anywhere but subscriptions", + descr: ["Returns the current user object, or null if not logged in. A reactive data source."] }; +Template.api.userLoaded = { + id: "meteor_userloaded", + name: "Meteor.userLoaded()", + locus: "Client", + descr: ["XXX"] +}; + + Template.api.userId = { id: "meteor_userid", name: "Meteor.userId()", - locus: "XXX client+methods", - descr: ["Returns the current user id, of null if not logged in. A reactive data source."] + locus: "Anywhere but subscriptions", + descr: ["Returns the current user id, or null if not logged in. A reactive data source."] }; @@ -733,7 +741,7 @@ Template.api.loginWithPassword = { Template.api.loginWithFacebook = { id: "meteor_loginwithfacebook", - name: "Meteor.loginWithFacebook([callback])", + name: "Meteor.loginWithFacebook([options], [callback])", locus: "Client", descr: ["Log the user in."], args: [ @@ -742,12 +750,19 @@ Template.api.loginWithFacebook = { type: "Function", descr: "XXX" } + ], + options: [ + { + name: "requestPermissions", + type: "Array of Strings", + descr: "XXX" + } ] }; Template.api.loginWithGithub = { id: "meteor_loginwithgithub", - name: "Meteor.loginWithGithub([callback])", + name: "Meteor.loginWithGithub([options], [callback])", locus: "Client", descr: ["Log the user in."], args: [ @@ -756,12 +771,19 @@ Template.api.loginWithGithub = { type: "Function", descr: "XXX" } + ], + options: [ + { + name: "requestPermissions", + type: "Array of Strings", + descr: "XXX" + } ] }; Template.api.loginWithGoogle = { id: "meteor_loginwithgoogle", - name: "Meteor.loginWithGoogle([callback])", + name: "Meteor.loginWithGoogle([options], [callback])", locus: "Client", descr: ["Log the user in."], args: [ @@ -770,6 +792,13 @@ Template.api.loginWithGoogle = { type: "Function", descr: "XXX" } + ], + options: [ + { + name: "requestPermissions", + type: "Array of Strings", + descr: "XXX" + } ] }; @@ -919,11 +948,30 @@ Template.api.accounts_resetPassword = { ] }; -Template.api.accounts_validateEmail = { - id: "accounts_validateemail", - name: "Accounts.validateEmail(token, [callback])", +Template.api.accounts_setPassword = { + id: "accounts_setpassword", + name: "Accounts.setPassword(userId, newPassword)", + locus: "Server", + descr: ["Force change the password for a user."], + args: [ + { + name: "userId", + type: "String", + descr: "XXX" + }, + { + name: "newPassword", + type: "String", + descr: "XXX" + } + ] +}; + +Template.api.accounts_verifyEmail = { + id: "accounts_verifyemail", + name: "Accounts.verifyEmail(token, [callback])", locus: "Client", - descr: ["Marks the user's email address as validated. Logs the user in afterwards."], + descr: ["Marks the user's email address as verified. Logs the user in afterwards."], args: [ { name: "token", @@ -939,23 +987,60 @@ Template.api.accounts_validateEmail = { }; - - -Template.api.accounts_config = { - id: "accounts_config", - name: "Accounts.config(options)", - locus: "Anywhere", - descr: ["Set global accounts options."], - options: [ +Template.api.accounts_sendResetPasswordEmail = { + id: "accounts_sendresetpasswordemail", + name: "Accounts.sendResetPasswordEmail(userId, [email])", + locus: "Server", + descr: ["Send an email with a link the user can use to reset their password."], + args: [ { - name: "validateEmail", - type: "Boolean", - descr: "If true, new users with an email address will receive a verifcation email." + name: "userId", + type: "String", + descr: "XXX" }, { - name: "forbidSignups", - type: "Boolean", - descr: "If true, new user signups will be rejected. Server-side user creation via XXXlinkify createUser is still allowed." + name: "email", + type: "String", + descr: "Optional. If passed, must be one of the email addresses in user.emails." + } + ] +}; + +Template.api.accounts_sendVerificationEmail = { + id: "accounts_sendverificationemail", + name: "Accounts.sendVerificationEmail(userId, [email])", + locus: "Server", + descr: ["Send an email with a link the user can use verify their email address."], + args: [ + { + name: "userId", + type: "String", + descr: "XXX" + }, + { + name: "email", + type: "String", + descr: "Optional. If passed, must be one of the email addresses in user.emails." + } + ] +}; + + +Template.api.accounts_sendEnrollmentEmail = { + id: "accounts_sendenrollmentemail", + name: "Accounts.sendEnrollmentEmail(userId, [email])", + locus: "Server", + descr: ["Send an email with a link the user can use to set their initial password."], + args: [ + { + name: "userId", + type: "String", + descr: "XXX" + }, + { + name: "email", + type: "String", + descr: "Optional. If passed, must be one of the email addresses in user.emails." } ] }; @@ -968,6 +1053,45 @@ Template.api.accounts_emailTemplates = { }; + +Template.api.accounts_config = { + id: "accounts_config", + name: "Accounts.config(options)", + locus: "Anywhere", + descr: ["Set global accounts options."], + options: [ + { + name: "sendVerificationEmail", + type: "Boolean", + descr: "If true, new users with an email address will receive an address verification email." + }, + { + name: "forbidClientAccountCreation", + type: "Boolean", + descr: "If true, new user signups will be rejected. Server-side user creation via XXXlinkify createUser is still allowed." + } + ] +}; + +Template.api.accounts_ui_config = { + id: "accounts_ui_config", + name: "Accounts.ui.config(options)", + locus: "Client", + descr: ["Set LoginButtons UI options."], + options: [ + { + name: "requestPermissions", + type: "Object", + descr: "what to pass to requestPermissions for each service. eg `{facebook: ['user_activity']}`" + }, + { + name: "passwordSignupFields", + type: "String", + descr: "'`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default)" + } + ] +}; + Template.api.accounts_validateNewUser = { id: "accounts_validatenewuser", name: "Accounts.validateNewUser(func)", @@ -997,50 +1121,6 @@ Template.api.accounts_onCreateUser = { }; -Template.api.accounts_setPassword = { - id: "accounts_setpassword", - name: "Accounts.setPassword(userId, newPassword)", - locus: "Server", - descr: ["Force change the password for a user."], - args: [ - { - name: "userId", - type: "String", - descr: "XXX" - }, - { - name: "newPassword", - type: "String", - descr: "XXX" - } - ] -}; - -Template.api.accounts_configuration = { - id: "accounts_configuration", - name: "Accounts.configuration", - locus: "Anywhere", - descr: ["A XXXlinkify Collection holding login service info."] -}; - - -Template.api.accounts_facebook_config = { - id: "accounts_facebook_config", - name: "Accounts.facebook.config(options)", - locus: "Anywhere", - descr: ["XXX REMOVE!"] -}; - -Template.api.accounts_google_config = { - id: "accounts_google_config", - name: "Accounts.google.config(options)", - locus: "Anywhere", - descr: ["XXX REMOVE!"] -}; - - - - Template.api.setTimeout = { diff --git a/docs/client/docs.js b/docs/client/docs.js index bc0b08b6b6..e1c5c2f008 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -177,6 +177,7 @@ var toc = [ "Meteor.user", "Meteor.userId", "Meteor.users", + "Meteor.userLoaded", "Meteor.logout", "Meteor.loginWithPassword", "Meteor.loginWithFacebook", @@ -190,19 +191,20 @@ var toc = [ "Accounts.changePassword", "Accounts.forgotPassword", "Accounts.resetPassword", - "Accounts.validateEmail", + "Accounts.setPassword", + "Accounts.verifyEmail", + {type: "spacer"}, + + "Accounts.sendResetPasswordEmail", + "Accounts.sendVerificationEmail", + "Accounts.sendEnrollmentEmail", + "Accounts.emailTemplates", {type: "spacer"}, "Accounts.config", - "Accounts.emailTemplates", + "Accounts.ui.config", "Accounts.validateNewUser", - "Accounts.onCreateUser", - "Accounts.setPassword", - "Accounts.configuration", - {type: "spacer"}, - - "Accounts.facebook.config", - "Accounts.google.config" + "Accounts.onCreateUser" ], From 78b262741f3950a07465496b3bec4cc550ec57ef Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 10 Oct 2012 21:16:20 -0700 Subject: [PATCH 6/9] Another pass through the docs. Almost all XXXs removed. --- docs/client/api.html | 48 +++++++-- docs/client/api.js | 245 +++++++++++++++---------------------------- docs/client/docs.js | 12 +-- 3 files changed, 129 insertions(+), 176 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 345eaea926..958378749d 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1639,16 +1639,27 @@ sub-template.

Accounts

+XXX intro text + {{> api_box user}} +- {_id: foo} if not userLoaded. +- schema / common fields + + {{> api_box userId}} {{> api_box users}} -XXX schema? +- on the client, current user. on the server all users. +- examples of usage? +- schema +- default publish and allow for profile {{> api_box userLoaded}} +- more text + {{#note}} We realize this is inconvenient. It is a temporary solution. In the future we will either make it unnecessary or fold it into a more @@ -1656,23 +1667,43 @@ general mechanism. {{/note}} {{> api_box logout}} + {{> api_box loginWithPassword}} -{{> api_box loginWithFacebook}} -{{> api_box loginWithGithub}} -{{> api_box loginWithGoogle}} -{{> api_box loginWithTwitter}} -{{> api_box loginWithWeibo}} + +{{> api_box loginWithOAuth}} + +- example scopes {{> api_box accounts_createUser}} + +- logs you in on the client +- diff between client and server +- username and/or email. which are optional. +- default user hook adds profile, override w/ onCreateUser +- not for oauth + {{> api_box accounts_changePassword}} + {{> api_box accounts_forgotPassword}} + +- triggers sendResetPasswordEmail +- document where the token goes in Accounts._whatever? +- youre responsibility to get it into resetPassword + {{> api_box accounts_resetPassword}} + +- don't need to call if you have accounts-ui + {{> api_box accounts_setPassword}} + {{> api_box accounts_verifyEmail}} +- pass token from sendVerificationEmail +- what changes in the schema + {{> api_box accounts_sendResetPasswordEmail}} -{{> api_box accounts_sendVerificationEmail}} {{> api_box accounts_sendEnrollmentEmail}} +{{> api_box accounts_sendVerificationEmail}} {{> api_box accounts_emailTemplates}} {{> api_box accounts_config}} @@ -1680,6 +1711,9 @@ general mechanism. {{> api_box accounts_validateNewUser}} {{> api_box accounts_onCreateUser}} +- takes `options`, `user` + +

Timers

diff --git a/docs/client/api.js b/docs/client/api.js index c1a268cc25..80f04a4631 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -671,23 +671,16 @@ Template.api.isolate = { Template.api.user = { id: "meteor_user", name: "Meteor.user()", - locus: "Anywhere but subscriptions", - descr: ["Returns the current user object, or null if not logged in. A reactive data source."] -}; - -Template.api.userLoaded = { - id: "meteor_userloaded", - name: "Meteor.userLoaded()", - locus: "Client", - descr: ["XXX"] + locus: "Anywhere but publish functions", + descr: ["Get the current user record, or `null` if no user is logged in. A reactive data source."] }; Template.api.userId = { id: "meteor_userid", name: "Meteor.userId()", - locus: "Anywhere but subscriptions", - descr: ["Returns the current user id, or null if not logged in. A reactive data source."] + locus: "Anywhere but publish functions", + descr: ["Get the current user id, or `null` if no user is logged in. A reactive data source."] }; @@ -695,9 +688,17 @@ Template.api.users = { id: "meteor_users", name: "Meteor.users", locus: "Anywhere", - descr: ["A XXXlinkify Collection holding user documents."] + descr: ["A Meteor.Collection containing user documents."] }; +Template.api.userLoaded = { + id: "meteor_userloaded", + name: "Meteor.userLoaded()", + locus: "Client", + descr: ["Determine if the current user document is fully loaded in Meteor.users. A reactive data source."] +}; + + Template.api.logout = { id: "meteor_logout", @@ -708,7 +709,7 @@ Template.api.logout = { { name: "callback", type: "Function", - descr: "XXX" + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ] }; @@ -718,156 +719,80 @@ Template.api.loginWithPassword = { id: "meteor_loginwithpassword", name: "Meteor.loginWithPassword(user, password, [callback])", locus: "Client", - descr: ["Log the user in."], + descr: ["Log the user in with a password."], args: [ { name: "user", type: "Object or String", - descr: "XXX" + descr: "Either a string interpreted as a username or an email; or an object with a single key: `email`, `username` or `id`." }, { name: "password", type: "String", - descr: "XXX" + descr: "The user's password. This is __not__ sent in plain text over the wire — it is secured with SRP." }, { name: "callback", type: "Function", - descr: "XXX" + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ] }; -Template.api.loginWithFacebook = { - id: "meteor_loginwithfacebook", - name: "Meteor.loginWithFacebook([options], [callback])", +Template.api.loginWithOAuth = { + id: "meteor_loginwithoauth", + name: "Meteor.loginWithOAuthProvider([options], [callback])", locus: "Client", - descr: ["Log the user in."], + descr: ["Log the user in using an external OAuth service."], args: [ { name: "callback", type: "Function", - descr: "XXX" + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ], options: [ { name: "requestPermissions", type: "Array of Strings", - descr: "XXX" + descr: "A list of permissions to request from the user." } ] }; -Template.api.loginWithGithub = { - id: "meteor_loginwithgithub", - name: "Meteor.loginWithGithub([options], [callback])", - locus: "Client", - descr: ["Log the user in."], - args: [ - { - name: "callback", - type: "Function", - descr: "XXX" - } - ], - options: [ - { - name: "requestPermissions", - type: "Array of Strings", - descr: "XXX" - } - ] -}; - -Template.api.loginWithGoogle = { - id: "meteor_loginwithgoogle", - name: "Meteor.loginWithGoogle([options], [callback])", - locus: "Client", - descr: ["Log the user in."], - args: [ - { - name: "callback", - type: "Function", - descr: "XXX" - } - ], - options: [ - { - name: "requestPermissions", - type: "Array of Strings", - descr: "XXX" - } - ] -}; - -Template.api.loginWithTwitter = { - id: "meteor_loginwithtwitter", - name: "Meteor.loginWithTwitter([callback])", - locus: "Client", - descr: ["Log the user in."], - args: [ - { - name: "callback", - type: "Function", - descr: "XXX" - } - ] -}; - -Template.api.loginWithWeibo = { - id: "meteor_loginwithweibo", - name: "Meteor.loginWithWeibo([callback])", - locus: "Client", - descr: ["Log the user in."], - args: [ - { - name: "callback", - type: "Function", - descr: "XXX" - } - ] -}; - - Template.api.accounts_createUser = { id: "accounts_createuser", - name: "Accounts.createUser(options, extra, [callback])", + name: "Accounts.createUser(options, [callback])", locus: "Anywhere", descr: ["Create a new user."], args: [ - { - name: "options", - type: "Object", - descr: "XXX" - }, - { - name: "extra", - type: "Object", - descr: "XXX" - }, { name: "callback", type: "Function", - descr: "XXX" + descr: "Client only, optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ], options: [ { name: "username", type: "String", - descr: "XXX" + descr: "A unique name for this user." }, { name: "email", type: "String", - descr: "XXX" + descr: "The user's email address." }, { name: "password", type: "String", - descr: "XXX" + descr: "The user's password. This is __not__ sent in plain text over the wire." + }, + { + name: "profile", + type: "Object", + descr: "The user's profile, typically including the `name` field." } ] }; @@ -876,22 +801,22 @@ Template.api.accounts_changePassword = { id: "accounts_changepassword", name: "Accounts.changePassword(oldPassword, newPassword, [callback])", locus: "Client", - descr: ["Change the users password. Must be logged in."], + descr: ["Change the current user's password. Must be logged in."], args: [ { name: "oldPassword", type: "String", - descr: "XXX" + descr: "The user's current password. This is __not__ sent in plain text over the wire." }, { name: "newPassword", type: "String", - descr: "XXX" + descr: "A new password for the user. This is __not__ sent in plain text over the wire." }, { name: "callback", type: "Function", - descr: "XXX" + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ] }; @@ -900,19 +825,19 @@ Template.api.accounts_forgotPassword = { id: "accounts_forgotpassword", name: "Accounts.forgotPassword(options, [callback])", locus: "Client", - descr: ["Request a forgot password email"], + descr: ["Request a forgot password email."], args: [ { name: "callback", type: "Function", - descr: "XXX" + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ], options: [ { name: "email", type: "String", - descr: "The email address to send a password reminder to." + descr: "The email address to send a password reset link." } ] }; @@ -921,48 +846,41 @@ Template.api.accounts_resetPassword = { id: "accounts_resetpassword", name: "Accounts.resetPassword(token, newPassword, [callback])", locus: "Client", - descr: ["Reset the password for a user, using the token from email. Logs you in afterwards."], + descr: ["Reset the password for a user using a token received in email. Logs the user in afterwards."], args: [ { name: "token", type: "String", - descr: "XXX" + descr: "The token retrieved from the reset password URL." }, { name: "newPassword", type: "String", - descr: "XXX" + descr: "A new password for the user. This is __not__ sent in plain text over the wire." }, { name: "callback", type: "Function", - descr: "XXX" + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ], - options: [ - { - name: "email", - type: "String", - descr: "The email address to send a password reminder to." - } - ] }; Template.api.accounts_setPassword = { id: "accounts_setpassword", name: "Accounts.setPassword(userId, newPassword)", locus: "Server", - descr: ["Force change the password for a user."], + descr: ["Forcibly change the password for a user."], args: [ { name: "userId", type: "String", - descr: "XXX" + descr: "The id of the user to update." }, { name: "newPassword", type: "String", - descr: "XXX" + descr: "A new password for the user." } ] }; @@ -976,12 +894,12 @@ Template.api.accounts_verifyEmail = { { name: "token", type: "String", - descr: "XXX" + descr: "The token retrieved from the verification URL." }, { name: "callback", type: "Function", - descr: "XXX" + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." } ] }; @@ -996,12 +914,31 @@ Template.api.accounts_sendResetPasswordEmail = { { name: "userId", type: "String", - descr: "XXX" + descr: "The id of the user to send email to." }, { name: "email", type: "String", - descr: "Optional. If passed, must be one of the email addresses in user.emails." + descr: "Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list." + } + ] +}; + +Template.api.accounts_sendEnrollmentEmail = { + id: "accounts_sendenrollmentemail", + name: "Accounts.sendEnrollmentEmail(userId, [email])", + locus: "Server", + descr: ["Send an email with a link the user can use to set their initial password."], + args: [ + { + name: "userId", + type: "String", + descr: "The id of the user to send email to." + }, + { + name: "email", + type: "String", + descr: "Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list." } ] }; @@ -1015,35 +952,17 @@ Template.api.accounts_sendVerificationEmail = { { name: "userId", type: "String", - descr: "XXX" + descr: "The id of the user to send email to." }, { name: "email", type: "String", - descr: "Optional. If passed, must be one of the email addresses in user.emails." + descr: "Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first unverified email in the list." } ] }; -Template.api.accounts_sendEnrollmentEmail = { - id: "accounts_sendenrollmentemail", - name: "Accounts.sendEnrollmentEmail(userId, [email])", - locus: "Server", - descr: ["Send an email with a link the user can use to set their initial password."], - args: [ - { - name: "userId", - type: "String", - descr: "XXX" - }, - { - name: "email", - type: "String", - descr: "Optional. If passed, must be one of the email addresses in user.emails." - } - ] -}; Template.api.accounts_emailTemplates = { id: "accounts_emailtemplates", @@ -1063,12 +982,12 @@ Template.api.accounts_config = { { name: "sendVerificationEmail", type: "Boolean", - descr: "If true, new users with an email address will receive an address verification email." + descr: "New users with an email address will receive an address verification email." }, { name: "forbidClientAccountCreation", type: "Boolean", - descr: "If true, new user signups will be rejected. Server-side user creation via XXXlinkify createUser is still allowed." + descr: "`createUser` requests from the client will be rejected." } ] }; @@ -1077,17 +996,17 @@ Template.api.accounts_ui_config = { id: "accounts_ui_config", name: "Accounts.ui.config(options)", locus: "Client", - descr: ["Set LoginButtons UI options."], + descr: ["Set Accounts UI options for the `loginButtons` template."], options: [ { name: "requestPermissions", type: "Object", - descr: "what to pass to requestPermissions for each service. eg `{facebook: ['user_activity']}`" + descr: "Which permissions to request from the user for each OAuth service. For example: `{facebook: ['user_likes'], github: ['user', 'repo']}`" }, { name: "passwordSignupFields", type: "String", - descr: "'`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default)" + descr: "Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default)." } ] }; @@ -1096,12 +1015,12 @@ Template.api.accounts_validateNewUser = { id: "accounts_validatenewuser", name: "Accounts.validateNewUser(func)", locus: "Server", - descr: ["Set up a new user validator."], + descr: ["Set restrictions on new user creation."], args: [ { name: "func", type: "Function", - descr: "Validator function. Takes a new user object, and returns true to allow the creation or false to abort." + descr: "Called whenever a new user is created. Takes the new user object, and returns true to allow the creation or false to abort." } ] }; @@ -1110,12 +1029,12 @@ Template.api.accounts_onCreateUser = { id: "accounts_oncreateuser", name: "Accounts.onCreateUser(func)", locus: "Server", - descr: ["Set up a new user mangler XXX."], + descr: ["Customize new user creation."], args: [ { name: "func", type: "Function", - descr: "Validator function. Takes `(options, extra, user)`. Return the new user, or throw an `Error`." + descr: "Called whenever a new user is created. Return the new user ojbject, or throw an `Error` to abort the creation." } ] }; diff --git a/docs/client/docs.js b/docs/client/docs.js index e1c5c2f008..c31a22bbd6 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -180,11 +180,11 @@ var toc = [ "Meteor.userLoaded", "Meteor.logout", "Meteor.loginWithPassword", - "Meteor.loginWithFacebook", - "Meteor.loginWithGithub", - "Meteor.loginWithGoogle", - "Meteor.loginWithTwitter", - "Meteor.loginWithWeibo", + {name: "Meteor.loginWithFacebook", id: "meteor_loginwithoauth"}, + {name: "Meteor.loginWithGithub", id: "meteor_loginwithoauth"}, + {name: "Meteor.loginWithGoogle", id: "meteor_loginwithoauth"}, + {name: "Meteor.loginWithTwitter", id: "meteor_loginwithoauth"}, + {name: "Meteor.loginWithWeibo", id: "meteor_loginwithoauth"}, {type: "spacer"}, "Accounts.createUser", @@ -196,8 +196,8 @@ var toc = [ {type: "spacer"}, "Accounts.sendResetPasswordEmail", - "Accounts.sendVerificationEmail", "Accounts.sendEnrollmentEmail", + "Accounts.sendVerificationEmail", "Accounts.emailTemplates", {type: "spacer"}, From ec54838d950d9fd045a4217616a5357dfe792f88 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 11 Oct 2012 14:53:00 -0700 Subject: [PATCH 7/9] Add accounts concepts section. --- docs/client/concepts.html | 25 +++++++++++++++++++++++++ docs/client/docs.js | 1 + 2 files changed, 26 insertions(+) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 964289b85f..58cc4c56ec 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -13,6 +13,7 @@ when writing those apps. {{> livehtml }} {{> templates }} {{> packages_concept }} +{{> accounts }} {{> deploying }} @@ -489,6 +490,30 @@ make your own packages just yet. Coming soon. {{/better_markdown}} + + + + +