diff --git a/docs/client/api.html b/docs/client/api.html index 3accf566b9..958378749d 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -120,6 +120,11 @@ is the place to stop the observes. {{> api_box subscription_stop}} +{{> 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 @@ -201,6 +206,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 @@ -215,6 +222,29 @@ 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 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}} + +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. + +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}} {{> api_box method_invocation_unblock}} @@ -623,6 +653,153 @@ Example: Logs.remove({}); +{{> api_box allow}} + +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. + +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`. + +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)"}} +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)"}} +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. + +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. + +{{/dtdd}} + +{{#dtdd "remove(userId, 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 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({ + 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'] // 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`. + +{{#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 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

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

Accounts

+ +XXX intro text + +{{> api_box user}} + +- {_id: foo} if not userLoaded. +- schema / common fields + + +{{> api_box userId}} + +{{> api_box users}} + +- 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 +general mechanism. +{{/note}} + +{{> api_box logout}} + +{{> api_box loginWithPassword}} + +{{> 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_sendEnrollmentEmail}} +{{> api_box accounts_sendVerificationEmail}} +{{> api_box accounts_emailTemplates}} + +{{> api_box accounts_config}} +{{> api_box accounts_ui_config}} +{{> api_box accounts_validateNewUser}} +{{> api_box accounts_onCreateUser}} + +- takes `options`, `user` + + +

Timers

Meteor uses global environment variables diff --git a/docs/client/api.js b/docs/client/api.js index 283ff58082..80f04a4631 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: ["The id of logged-in user, or `null` if no user is logged in."] +}; + + 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: ["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 the logged in user."], + args: [ + {name: "userId", + type: "String or null", + descr: "The value that should be returned by `userId` on this connection."} + ] +}; + Template.api.method_invocation_unblock = { id: "method_unblock", name: "this.unblock()", @@ -371,6 +398,93 @@ 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.allow = { + id: "allow", + name: "collection.allow(options)", + locus: "Server", + 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: "Functions that look at a proposed modification to the database and return true if it should be allowed."}, + {name: "fetch", + 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."} + ] +}; + +Template.api.deny = { + id: "deny", + name: "collection.deny(options)", + locus: "Server", + descr: ["Override `allow` rules."], + options: [ + {name: "insert, update, remove", + type: "Function", + 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: "Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions."} + ] +}; + + Template.api.cursor_count = { id: "count", name: "cursor.count()", @@ -429,62 +543,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" @@ -610,6 +668,380 @@ Template.api.isolate = { +Template.api.user = { + id: "meteor_user", + name: "Meteor.user()", + 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 publish functions", + descr: ["Get the current user id, or `null` if no user is logged in. A reactive data source."] +}; + + +Template.api.users = { + id: "meteor_users", + name: "Meteor.users", + locus: "Anywhere", + 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", + name: "Meteor.logout([callback])", + locus: "Client", + descr: ["Log the user out."], + args: [ + { + name: "callback", + type: "Function", + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." + } + ] +}; + + +Template.api.loginWithPassword = { + id: "meteor_loginwithpassword", + name: "Meteor.loginWithPassword(user, password, [callback])", + locus: "Client", + descr: ["Log the user in with a password."], + args: [ + { + name: "user", + type: "Object or String", + 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: "The user's password. This is __not__ sent in plain text over the wire — it is secured with SRP." + }, + { + name: "callback", + type: "Function", + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." + } + ] +}; + + +Template.api.loginWithOAuth = { + id: "meteor_loginwithoauth", + name: "Meteor.loginWithOAuthProvider([options], [callback])", + locus: "Client", + descr: ["Log the user in using an external OAuth service."], + args: [ + { + name: "callback", + type: "Function", + 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: "A list of permissions to request from the user." + } + ] +}; + +Template.api.accounts_createUser = { + id: "accounts_createuser", + name: "Accounts.createUser(options, [callback])", + locus: "Anywhere", + descr: ["Create a new user."], + args: [ + { + name: "callback", + type: "Function", + 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: "A unique name for this user." + }, + { + name: "email", + type: "String", + descr: "The user's email address." + }, + { + name: "password", + type: "String", + 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." + } + ] +}; + +Template.api.accounts_changePassword = { + id: "accounts_changepassword", + name: "Accounts.changePassword(oldPassword, newPassword, [callback])", + locus: "Client", + descr: ["Change the current user's password. Must be logged in."], + args: [ + { + name: "oldPassword", + type: "String", + descr: "The user's current password. This is __not__ sent in plain text over the wire." + }, + { + name: "newPassword", + type: "String", + descr: "A new password for the user. This is __not__ sent in plain text over the wire." + }, + { + name: "callback", + type: "Function", + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." + } + ] +}; + +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: "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 reset link." + } + ] +}; + +Template.api.accounts_resetPassword = { + id: "accounts_resetpassword", + name: "Accounts.resetPassword(token, newPassword, [callback])", + locus: "Client", + descr: ["Reset the password for a user using a token received in email. Logs the user in afterwards."], + args: [ + { + name: "token", + type: "String", + descr: "The token retrieved from the reset password URL." + }, + { + name: "newPassword", + type: "String", + descr: "A new password for the user. This is __not__ sent in plain text over the wire." + }, + { + name: "callback", + type: "Function", + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." + } + ], +}; + +Template.api.accounts_setPassword = { + id: "accounts_setpassword", + name: "Accounts.setPassword(userId, newPassword)", + locus: "Server", + descr: ["Forcibly change the password for a user."], + args: [ + { + name: "userId", + type: "String", + descr: "The id of the user to update." + }, + { + name: "newPassword", + type: "String", + descr: "A new password for the user." + } + ] +}; + +Template.api.accounts_verifyEmail = { + id: "accounts_verifyemail", + name: "Accounts.verifyEmail(token, [callback])", + locus: "Client", + descr: ["Marks the user's email address as verified. Logs the user in afterwards."], + args: [ + { + name: "token", + type: "String", + descr: "The token retrieved from the verification URL." + }, + { + name: "callback", + type: "Function", + descr: "Optional callback. Called with no arguments on success, or with a single `Error` argument on failure." + } + ] +}; + + +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: "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." + } + ] +}; + +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." + } + ] +}; + +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: "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 unverified email in the list." + } + ] +}; + + + +Template.api.accounts_emailTemplates = { + id: "accounts_emailtemplates", + name: "Accounts.emailTemplates", + locus: "Anywhere", + descr: ["XXX"] +}; + + + +Template.api.accounts_config = { + id: "accounts_config", + name: "Accounts.config(options)", + locus: "Anywhere", + descr: ["Set global accounts options."], + options: [ + { + name: "sendVerificationEmail", + type: "Boolean", + descr: "New users with an email address will receive an address verification email." + }, + { + name: "forbidClientAccountCreation", + type: "Boolean", + descr: "`createUser` requests from the client will be rejected." + } + ] +}; + +Template.api.accounts_ui_config = { + id: "accounts_ui_config", + name: "Accounts.ui.config(options)", + locus: "Client", + descr: ["Set Accounts UI options for the `loginButtons` template."], + options: [ + { + name: "requestPermissions", + type: "Object", + 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: "Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default)." + } + ] +}; + +Template.api.accounts_validateNewUser = { + id: "accounts_validatenewuser", + name: "Accounts.validateNewUser(func)", + locus: "Server", + descr: ["Set restrictions on new user creation."], + args: [ + { + name: "func", + type: "Function", + descr: "Called whenever a new user is created. Takes the 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: ["Customize new user creation."], + args: [ + { + name: "func", + type: "Function", + descr: "Called whenever a new user is created. Return the new user ojbject, or throw an `Error` to abort the creation." + } + ] +}; + + + + Template.api.setTimeout = { id: "meteor_settimeout", name: "Meteor.setTimeout", diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 964289b85f..ca1a4a4640 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 }} @@ -207,6 +208,9 @@ And the reactive data sources that can trigger changes are: * Session variables * Database queries on Collections * `Meteor.status` +* `Meteor.user` +* `Meteor.userId` +* `Meteor.userLoaded` Meteor's implementation of reactivity is short and sweet, about 50 lines of code. You can @@ -489,6 +493,30 @@ make your own packages just yet. Coming soon. {{/better_markdown}} + + + + +