{"result":{"depiction_url":null,"edges":{"in_module":{"objects":[{"created":"2020-05-30T05:47:50Z","object_id":{"id":2010,"is_a":["text","documentation","reference","module"],"name":"doc_module_mod_signup","title":"mod_signup","uri":"https:\/\/zotonic.com\/id\/2010"},"seq":1000000}],"predicate":{"id":333,"is_a":["meta","predicate"],"name":"in_module","title":{"_type":"trans","tr":{"en":"In module"}},"uri":"https:\/\/zotonic.com\/id\/in_module"}},"subject":{"objects":[{"created":"2026-09-09T13:47:55Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-09-09T13:47:55Z","object_id":{"id":2564,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_backend_developer","title":"Backend developer","uri":"https:\/\/zotonic.com\/id\/2564"},"seq":2},{"created":"2026-09-09T13:47:55Z","object_id":{"id":2582,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_forms","title":"Forms","uri":"https:\/\/zotonic.com\/id\/2582"},"seq":3},{"created":"2026-09-09T13:47:55Z","object_id":{"id":2589,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_identity_and_accounts","title":"Identity and accounts","uri":"https:\/\/zotonic.com\/id\/2589"},"seq":4},{"created":"2026-09-09T13:47:55Z","object_id":{"id":2590,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_authentication","title":"Authentication","uri":"https:\/\/zotonic.com\/id\/2590"},"seq":5},{"created":"2026-09-09T13:47:55Z","object_id":{"id":2626,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_controller","title":"Controller","uri":"https:\/\/zotonic.com\/id\/2626"},"seq":6},{"created":"2026-09-09T13:47:55Z","object_id":{"id":2637,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_create","title":"Create","uri":"https:\/\/zotonic.com\/id\/2637"},"seq":7}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":1895,"is_a":["text","documentation","reference","controller"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1895"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_controller_controller_signup","page_url":{"en":"https:\/\/zotonic.com\/docs\/1895\/controller_signup","x-default":"https:\/\/zotonic.com\/docs\/1895\/controller_signup"},"preview_url":null,"resource":{"version":3326,"pivot_location_lat":null,"title":"controller_signup","is_authoritative":true,"body":"<p>Controller which displays a form to sign up (rendered from <code>signup.tpl<\/code>).<\/p>\n<p>It also implements the necessary postbacks to perform the signup and log a user in.<\/p>\n<h2>Data flow<\/h2>\n<p>The signup page can be reached directly through the <code>signup<\/code> dispatch rule, or by\nasking the notification system for a <code>#signup_url{}<\/code>. The <code>mod_signup<\/code>\n<code>observe_signup_url\/2<\/code> observer stores the caller supplied arguments in\n<code>mod_server_storage<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">#signup_url{\n    props = Props,\n    signup_props = SignupProps\n}\n<\/code><\/pre>\n<p><code>Props<\/code> are resource properties for the person that will be created or updated,\nfor example a prefilled <code>email<\/code>, <code>name_first<\/code>, or <code>depiction_url<\/code>. <code>SignupProps<\/code>\nare signup control values and identities, for example <code>{user_id, Id}<\/code>,\n<code>{ready_page, Url}<\/code>, or <code>{identity, {Type, Key, IsUnique, IsVerified}}<\/code>.<\/p>\n<p>The observer generates a random check id, stores\n<code>{CheckId, Props, SignupProps}<\/code>, and returns the signup URL with <code>xs=CheckId<\/code>.\nWhen this controller renders the page it reads the <code>xs<\/code> query argument, performs\n<code>m_server_storage:secure_lookup\/2<\/code>, and only accepts the stored payload when the\nreturned check id matches the query value. Accepted values are exposed to\n<code>signup.tpl<\/code> as:<\/p>\n<ul><li><code>props<\/code>: a map with prefilled resource properties. Empty email values are\nremoved so the email step can still ask for an address.<\/li><li><code>signup_props<\/code>: the stored signup properties as a proplist.<\/li><li><code>email<\/code>: the prefilled email from <code>props<\/code>, when present.<\/li><\/ul>\n<p>Without a valid <code>xs<\/code>, the controller renders with empty signup properties and\ndefault <code>props<\/code>. The <code>xs<\/code> value itself is not posted back. Instead, the stored\npayload is carried forward as template variables in each wire postback\n(<code>props=props signup_props=signup_props<\/code>). This keeps the browser-visible form\nstate limited to the values the template already needs to render, while the\noriginal <code>xs<\/code> token is only used to bootstrap the first page render.<\/p>\n<h2>Handled events<\/h2>\n<p>The controller accepts these postbacks:<\/p>\n<ul><li><code>signup_email_step1<\/code>: validates and prechecks the email address. If the address\nis new and local signup is possible, a short one-time code is mailed using\n<code>email_signup_code.tpl<\/code>. If the address is already known, or an external\nprovider handles the domain, the second step shows a logon link and\/or external\nprovider buttons instead.<\/li><li><code>signup_email_step2<\/code>: checks the mailed one-time code. On success, the code is\ndeleted and the account details form is rendered.<\/li><li><code>signup_email_step3<\/code>: verifies the email did not change, checks the terms\ncheckbox, merges posted form properties with any prefilled <code>props<\/code>, combines\nidentities from <code>signup_props<\/code> with the verified email identity, creates or\nupdates the user through <code>mod_signup:signup_existing\/5<\/code>, logs the user on, and\nsends a one-time authentication token to the client auth model for redirect.<\/li><li><code>signup_go_step1<\/code>: returns the browser UI to the first email step.<\/li><li><code>signup_resend_code<\/code>: replaces the stored one-time code for the email address\nand sends a new <code>email_signup_code.tpl<\/code> message.<\/li><\/ul>\n<h2>Template structure<\/h2>\n<p>The public signup page is <code>signup.tpl<\/code>. It extends <code>base.tpl<\/code>, adds the logon\nCSS in <code>html_head_extra<\/code>, and fills <code>content_area<\/code> by including\n<code>_signup_box.tpl<\/code>. Other pages can include <code>_signup_box.tpl<\/code> directly when they\nneed the signup UI without the surrounding page inheritance. The signup box\nhandles already logged-on users and otherwise includes:<\/p>\n<ul><li><code>_signup_with_email.tpl<\/code> for the three-step email signup flow.<\/li><li>all <code>_logon_extra.tpl<\/code> templates supplied by active authentication modules for\nSSO signup options.<\/li><\/ul>\n<p><code>_signup_with_email.tpl<\/code> includes <code>_signup_with_email_step1.tpl<\/code> for the first\nemail form and renders empty containers for steps two and three. The controller\nfills those containers with <code>_signup_with_email_step2.tpl<\/code> and\n<code>_signup_with_email_step3.tpl<\/code> after the corresponding postbacks.<\/p>\n<p><code>_signup_with_email_step1.tpl<\/code> renders the email address form and posts\n<code>signup_email_step1<\/code>. If an email address was supplied through <code>props<\/code>, it shows\nthat address with a Change link. The Change postback re-renders\n<code>_signup_with_email_step1.tpl<\/code> without the prefilled email so the visitor can\nenter another address.<\/p>\n<p><code>_signup_with_email_step2.tpl<\/code> shows the selected email address, the mailed code\nform, send status, resend action, existing-account logon link, external-provider\noptions, and error states.<\/p>\n<p><code>_signup_with_email_step3.tpl<\/code> renders the final signup form. It defines blocks\naround the form, field set, individual field groups, and error area so sites can\noverride the shape of the final step. It uses the configuration exposed through\n<code>m.signup.config.username_equals_email<\/code> to decide whether the username is hidden\nand equal to the email address, or entered separately by the user.<\/p>\n<h2>Email address confirmation<\/h2>\n<p>There are two email checks in the complete signup lifecycle.<\/p>\n<p>The first check happens before account creation. <code>signup_email_step1<\/code> sends the\nshort code rendered by <code>email_signup_code.tpl<\/code>. Codes are stored in the\n<code>mod_signup<\/code> gen_server under the tag <code>{signup, EmailNorm}<\/code>. They are replaced on\nresend, expire by generational garbage collection, are rate-limit checked through\n<code>#auth_precheck{}<\/code>, and are deleted after a successful <code>signup_email_step2<\/code>.\nThis confirms that the visitor can read the mailbox before the account is\ncreated. The verified email is then added to the signup identities as\n<code>{identity, {email, Email, false, true}}<\/code> unless an email identity was already\nsupplied in <code>signup_props<\/code>.<\/p>\n<p>The second check is the account identity confirmation handled by <code>mod_signup<\/code>\nand <code>controller_signup_confirm<\/code>. If signup is requested with confirmation\nenabled and no non-password identity is already verified, <code>mod_signup<\/code> creates\nthe user unpublished and unverified, inserts unverified identities, and sends\n<code>email_verify.tpl<\/code> through the <code>#identity_verification{}<\/code> observer. The email\ncontains a <code>signup_confirm<\/code> URL with the identity verification key.<\/p>\n<p><code>controller_signup_confirm<\/code> renders <code>signup_confirm.tpl<\/code>. That template extends\n<code>base.tpl<\/code> and immediately posts the key back to the confirmation controller.\nThe confirmation controller looks up the identity by verification key, publishes\nthe user resource, marks the account and identity as verified, emits\n<code>#signup_confirm{id=UserId}<\/code>, logs the user on, and redirects to the first\n<code>#signup_confirm_redirect{}<\/code> result or to the user&#39;s page.<\/p>","slug":"controller_signup","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"a04619c8769e43119710fd2f02b9ee985ef0faeb435d6c0b71e260db554702a0","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":321,"is_a":["meta","category"],"name":"controller","title":"Controllers","uri":"https:\/\/test.zotonic.com\/id\/321"},"doc_source_path":"apps\/zotonic_mod_signup\/src\/controllers\/controller_signup.erl","publication_start":"2023-01-30T19:24:02Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_signup\/src\/controllers\/controller_signup.erl","pivot_location_lng":null,"doc_source_kind":"controller","name":"doc_controller_controller_signup","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2020-05-30T05:47:42Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_signup","publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"b8c4b2ccdc223257f311968fbd17b2d66f0c4156\n","erlang_module":"controller_signup","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-09-30T09:39:42Z","title_slug":"controller_signup"},"uri":"https:\/\/zotonic.com\/id\/1895","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1895"}},"status":"ok"}