Metadata-Version: 2.4
Name: splent_feature_signup
Version: 0.3.0
License-Expression: LGPL-3.0-or-later
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: COPYING
License-File: COPYING.LESSER
Requires-Dist: splent_framework>=1.15.0
Dynamic: license-file

# splent_feature_signup

The `signup` feature of a [SPLENT](https://splent.io) product line.

Public self-registration. Without it a product is closed and an administrator
gives each person access. With it, `/signup/` opens an account with an email
address and a password, and a "Sign up" link appears on the login page, in the
anonymous navbar and in the anonymous sidebar. It needs `auth`, which owns the
account.

A SPLENT feature is a Python package that a product composes with others.
Its models, routes, services, templates and hooks are registered when the
product starts. What this feature provides and requires is listed in the
`[tool.splent.contract]` section of `pyproject.toml`, which
`splent feature:contract --write` keeps in sync with the source.

## What a sign-up does

1. The form is validated, with every field other features registered.
2. `signup-submitting` is sent, and any listener may refuse.
3. The address is checked, and the account is created through auth's
   `AuthenticationService.create_user`, always with the role `user`.
4. auth sends `user-registered`, with the values of the registered fields.
5. The account opens at once, the person is logged in and goes to `next`,
   or where a login would send them. When the product confirms addresses
   (a feature registers `ConfirmemailService`), the account waits closed and
   the person is sent to the login page to read where the link went.

## Password

The form follows auth's password policy, the same one as every other form
that sets a password. signup has no setting of its own.

| Setting (auth) | Default | On the sign-up form |
|---|---|---|
| `AUTH_PASSWORD_MIN_LENGTH` | 8 | The shortest password accepted, from 8 to 128. No password longer than 128 characters is accepted |
| `AUTH_PASSWORD_CONFIRM` | false | When true, a second box asks for the password again |

```toml
[tool.splent.config]
AUTH_PASSWORD_MIN_LENGTH = 12
AUTH_PASSWORD_CONFIRM = true
```

With an auth older than its password policy, which has no `passwords`
module, the form keeps the rule it had before. It asks once, for at least 8
characters, and the two settings change nothing.

## Fields other features add

A feature that keeps something about the person asks for it at sign-up with
`register_signup_field`. The field is drawn on the page, validated with the
form, and its value travels on `user-registered` under its name.

```python
# __init__.py of a profile feature
from flask_babel import lazy_gettext as _l
from wtforms import StringField
from wtforms.validators import DataRequired, Length

from splent_io.splent_feature_signup.forms import register_signup_field


def init_feature(app):
    register_signup_field(
        "name",
        StringField(_l("Name"), validators=[DataRequired(), Length(max=100)]),
        order=10,
    )
    register_signup_field(
        "surname",
        StringField(_l("Surname"), validators=[DataRequired(), Length(max=100)]),
        order=20,
    )
```

```python
# signals.py of the same feature
@connect_signal("user-registered", "splent_feature_profile")
def keep_the_name(sender, user, name="", surname="", **kwargs):
    ...
```

`order` places the field. Lower comes first.

| Field | Order |
|---|---|
| A registered field that says so | its own |
| `email` | 100 (`EMAIL_ORDER`) |
| `password`, and its repeat | 200 (`PASSWORD_ORDER`) |
| A registered field that does not say | 300 (`DEFAULT_FIELD_ORDER`) |

The registry is read when a request needs the form, so a field registered
after signup has loaded is asked for too. The form class is built once for
each set of registered fields and reused while that set stays the same.
Registering a name again replaces the earlier field. `remove_signup_field(name)` stops asking for one. A field's own
`render_kw` (a placeholder) and `description` (a line of help) are shown, and
a `BooleanField` is drawn as a checkbox, so a feature can ask to accept its
terms with `BooleanField(_l("I accept the terms"), validators=[DataRequired()])`.

Some names are refused with a `ValueError` when the field is registered.
`role` and `active` decide who the account is and whether it opens, which is
never the visitor's choice. `user` and `sender` are the arguments of
`user-registered`. The form's own fields (`email`, `password`,
`confirm_password`, `next`, `submit`, `csrf_token`) and the form's attributes
(`data`, `errors`, `meta`...) cannot be taken either.

## Checks before the account is created

`signup-submitting` is sent once the form is valid and before the address is
looked up, with `form` (the posted form) and `remoteip`. A listener that
returns `False` refuses the sign-up. The form comes back with the typed values
and "We could not verify this sign-up. Please try again.", and no account is
made. `None` or `True` let it through. A captcha provider listens here, as it
listens to `contact-submitting` and `comment-submitting`, and draws its widget
in the `signup.form_fields` slot.

```python
# signals.py of a captcha feature
from splent_framework.signals.signal_utils import connect_signal


@connect_signal("signup-submitting", "splent_feature_mycaptcha")
def check_the_answer(sender, form=None, remoteip=None, **kwargs):
    from splent_io.splent_feature_mycaptcha.services import MyCaptchaService

    return MyCaptchaService().verify(form.get("my-captcha-answer"), remoteip)
```

## The sign-up page

The page opens three slots. A callback takes no arguments and returns HTML,
or an empty string to add nothing on this request.

| Slot | Where it renders | When |
|---|---|---|
| `signup.form_fields` | Inside the form, after the fields and right above the button | Always |
| `signup.providers` | After the form, under the words "or continue with" | Only when a callback returns something. Otherwise nothing is drawn, not even the words |
| `signup.form_footer` | At the bottom of the form, under "Already have an account?" | Always |

```python
# hooks.py of a captcha feature
from flask import render_template
from splent_framework.hooks.template_hooks import register_template_hook


def captcha_widget():
    return render_template("hooks/captcha_widget.html")


register_template_hook("signup.form_fields", captcha_widget)
```

An external sign-in provider puts its button in `signup.providers`, as it
does in auth's `auth.login.providers`.

```python
register_template_hook("signup.providers", sign_in_button, order=20)
```

The page extends `AUTH_LOGIN_LAYOUT`, the shell of the login page, so the two
look like one site.

## Where the person lands

A sign-up on the way to a page ends up on that page. `/signup/?next=/the/page`
goes to `/the/page` once the account is open, and to
`/login?next=/the/page` while it waits for its confirmation. The rule is
auth's `safe_next_url`, so only a path on this site is followed and anything
else is ignored. The "Sign up!" link on the login page carries the login's
`next`, and the sign-up page's "Log in" link carries it back.

## Use it in a product

```bash
splent feature:add splent_feature_signup
```

## Develop

```bash
splent feature:test splent_feature_signup
splent feature:contract splent_feature_signup
```

## Release

```bash
splent feature:release splent_feature_signup --dry-run --bump patch
splent feature:release splent_feature_signup
```

The first command rehearses the release and changes nothing. The second
publishes it to the git forge and to PyPI, and a PyPI version is permanent.
