Configuration¶
Load the configuration¶
Canaille can be configured either by a environment variables, environment file, or by a configuration file.
Configuration file¶
- CONFIG¶
The configuration can be written in toml configuration file which path is passed in the
CONFIG
environment variable.
SECRET_KEY = "very-secret"
[CANAILLE]
NAME = "My organization"
[CANAILLE_SQL]
DATABASE_URI = "postgresql://user:password@localhost/database"
...
You can have a look at the example file for inspiration.
Environment variables¶
In addition, parameters that have not been set in the configuration file can be read from environment variables. The way environment variables are parsed can be read from the pydantic-settings documentation.
Tip
For environment vars, the separator between sections and variables is a double underscore: __
.
For instance, the NAME
var in the CANAILLE
section shown above is CANAILLE__NAME
.
Environment file¶
Any environment variable can also be written in an environment file, which path should be passed in the ENV_FILE
environment variable.
For instance, set ENV_FILE=.env
to load a .env
file.
SECRET_KEY="very-secret"
CANAILLE__NAME="My organization"
CANAILLE_SQL__DATABASE_URI="postgresql://user:password@localhost/database"
Configuration methods priority¶
If a same configuration option is defined by different ways, here is how Canaille will choose which one to use:
environment vars have priority over the environment file and the configuration file;
environment file will have priority over the configuration file.
Parameters¶
- canaille.app.configuration.RootSettings[source]¶
The top-level namespace contains the configuration settings unrelated to Canaille.
The configuration parameters from the following libraries can be used:
config.toml¶SECRET_KEY = "very-secret" SERVER_NAME = "auth.mydomain.example" PREFERRED_URL_SCHEME = false DEBUG = false [CANAILLE] NAME = "My organization" ...
- DEBUG: bool = False¶
The Flask
DEBUG
configuration setting.This enables debug options.
Danger
This is useful for development but should be absolutely avoided in production environments.
- PREFERRED_URL_SCHEME: str = 'https'¶
The Flask
PREFERRED_URL_SCHEME
configuration setting.This sets the url scheme by which canaille will be served.
- SECRET_KEY: str [Required]¶
The Flask
SECRET_KEY
configuration setting.You MUST change this.
- SERVER_NAME: str | None = None¶
The Flask
SERVER_NAME
configuration setting.This sets domain name on which canaille will be served.
- canaille.core.configuration.CoreSettings[source]¶
The settings from the
CANAILLE
namespace.Those are all the configuration parameters that controls the behavior of Canaille.
- ACL: dict[str, ACLSettings] | None = {'DEFAULT': ACLSettings(PERMISSIONS=[<Permission.EDIT_SELF: 'edit_self'>, <Permission.USE_OIDC: 'use_oidc'>], READ=['user_name', 'groups', 'lock_date'], WRITE=['photo', 'given_name', 'family_name', 'display_name', 'password', 'phone_numbers', 'emails', 'profile_url', 'formatted_address', 'street', 'postal_code', 'locality', 'region', 'preferred_language', 'employee_number', 'department', 'title', 'organization'], FILTER=None)}¶
Mapping of permission groups. See
ACLSettings
for more details.The ACL name can be freely chosen. For example:
..code-block:: toml
[CANAILLE.ACL.DEFAULT] PERMISSIONS = [“edit_self”, “use_oidc”] READ = [“user_name”, “groups”] WRITE = [“given_name”, “family_name”]
[CANAILLE.ACL.ADMIN] WRITE = [“user_name”, “groups”]
- ADMIN_EMAIL: str | None = None¶
Administration email contact.
In certain special cases (example : questioning about password corruption), it is necessary to provide an administration contact email.
- EMAIL_CONFIRMATION: bool = True¶
If
True
, users will need to click on a confirmation link sent by email when they want to add a new email.By default, this is true if
SMTP
is configured, else this is false. If explicitly set to true andSMTP
is disabled, the email field will be read-only.
- EMAIL_OTP: bool = False¶
If
True
, then users will need to authenticate themselves via a one-time password sent to their primary email address.
- ENABLE_INTRUDER_LOCKOUT: bool = False¶
If
True
, then users will have to wait for an increasingly long time between each failed login attempt.
- ENABLE_PASSWORD_COMPROMISSION_CHECK: bool = False¶
If
True
, Canaille will check if passwords appears in compromission databases such as HIBP when users choose a new one.
- ENABLE_PASSWORD_RECOVERY: bool = True¶
If
False
, then users cannot ask for a password recovery link by email.
- ENABLE_REGISTRATION: bool = False¶
If
True
, then users can freely create an account at this instance.If email verification is available, users must confirm their email before the account is created.
- HIDE_INVALID_LOGINS: bool = True¶
If
True
, when users try to sign in with an invalid login, a message is shown indicating that the password is wrong, but does not give a clue whether the login exists or not.If
False
, when a user tries to sign in with an invalid login, a message is shown indicating that the login does not exist.
- INVITATION_EXPIRATION: int = 172800¶
The validity duration of registration invitations, in seconds.
Defaults to 2 days.
- LANGUAGE: str | None = None¶
If a language code is set, it will be used for every user.
If unset, the language is guessed according to the users browser.
- LOGGING: str | dict | None = None¶
Configures the logging output using the python logging configuration format:
If
None
, everything is logged in the standard error output. The log level isDEBUG
if theDEBUG
setting isTrue
, else this isINFO
.If this is a
dict
, it is passed tologging.config.dictConfig()
:If this is a
str
, it is expected to be a file path that will be passed tologging.config.fileConfig()
.
For example:
..code-block:: toml
[CANAILLE.LOGGING] version = 1 formatters.default.format = “[%(asctime)s] %(levelname)s in %(module)s: %(message)s” root = {level = “INFO”, handlers = [“canaille”]}
[CANAILLE.LOGGING.handlers.canaille] class = “logging.handlers.WatchedFileHandler” filename = “/var/log/canaille.log” formatter = “default”
- LOGO: str | None = None¶
The logo of your organization, this is useful to make your organization recognizable on login screens.
- MAX_PASSWORD_LENGTH: int = 1000¶
User password maximum length.
There is a technical of 4096 characters with the SQL backend. If the value is 0,
None
, or greater than 4096, then 4096 will be retained.
- MIN_PASSWORD_LENGTH: int = 8¶
User password minimum length.
If 0 or
None
, password won’t have a minimum length.
- OTP_METHOD: str = None¶
If OTP_METHOD is defined, then users will need to authenticate themselves using a one-time password (OTP) via an authenticator app. If set to
TOTP
, the application will use time one-time passwords, If set toHOTP
, the application will use HMAC-based one-time passwords.
- PASSWORD_COMPROMISSION_CHECK_API_URL: str = 'https://api.pwnedpasswords.com/range/'¶
Have i been pwned api url for compromission checks.
- PASSWORD_LIFETIME: str | None = None¶
Password validity duration.
If set, user passwords expire after this delay. Users are forced to change their password when the lifetime of the password is over. The duration value is expressed in ISO8601 format. For example, delay of 60 days is written “P60D”.
- SENTRY_DSN: str | None = None¶
A Sentry DSN to collect the exceptions.
This is useful for tracking errors in test and production environments.
- SMPP: SMPPSettings | None = None¶
The settings related to SMPP configuration.
If unset, sms-related features like sms one-time passwords won’t be enabled.
- SMS_OTP: bool = False¶
If
True
, then users will need to authenticate themselves via a one-time password sent to their primary phone number.
- SMTP: SMTPSettings | None = None¶
The settings related to SMTP and mail configuration.
If unset, mail-related features like password recovery won’t be enabled.
- THEME: str = 'default'¶
The name of a theme in the ‘theme’ directory, or a path to a theme.
Defaults to
default
. Theming is done with flask-themer.
- canaille.core.configuration.SMTPSettings[source]¶
The SMTP configuration. Belong in the
CANAILLE.SMTP
namespace. If unset, mail related features will be disabled, such as mail verification or password recovery emails.By default, Canaille will try to send mails from localhost without authentication.
- canaille.core.configuration.SMPPSettings[source]¶
The SMPP configuration. Belong in the
CANAILLE.SMPP
namespace. If not set, sms related features such as sms one-time passwords will be disabled.
- canaille.core.configuration.ACLSettings[source]¶
Access Control List settings. Belong in the
CANAILLE.ACL
namespace.You can define access controls that define what users can do on canaille. An access control consists in a
FILTER
to match users, a list ofPERMISSIONS
matched users will be able to perform, and fields users will be able toREAD
andWRITE
. Users matching several filters will cumulate permissions.- FILTER: dict[str, str] | list[dict[str, str]] | None = None¶
FILTER
can be:None
, in which case all the users will match this access controla mapping where keys are user attributes name and the values those user attribute values. All the values must be matched for the user to be part of the access control.
a list of those mappings. If a user values match at least one mapping, then the user will be part of the access control
Here are some examples:
FILTER = {user_name = 'admin'} FILTER = [ {groups = 'admins}, {groups = 'moderators'}, ]
- PERMISSIONS: list[Permission] = [Permission.EDIT_SELF, Permission.USE_OIDC]¶
A list of
Permission
users in the access control will be able to manage.For example:
..code-block:: toml
PERMISSIONS = [“manage_users”, “manage_groups”, “manage_oidc”, “delete_account”, “impersonate_users”]
- READ: list[str] = ['user_name', 'groups', 'lock_date']¶
A list of
User
attributes that users in the ACL will be able to read.
- WRITE: list[str] = ['photo', 'given_name', 'family_name', 'display_name', 'password', 'phone_numbers', 'emails', 'profile_url', 'formatted_address', 'street', 'postal_code', 'locality', 'region', 'preferred_language', 'employee_number', 'department', 'title', 'organization']¶
A list of
User
attributes that users in the ACL will be able to edit.
- class canaille.core.configuration.Permission(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
The permissions that can be assigned to users.
The permissions are intended to be used in
ACLSettings
.- DELETE_ACCOUNT = 'delete_account'¶
Allows users to delete their account.
If used with
MANAGE_USERS
, users can delete any account.
- EDIT_SELF = 'edit_self'¶
Allows users to edit their own profile.
- IMPERSONATE_USERS = 'impersonate_users'¶
Allows users to take the identity of another user.
- MANAGE_GROUPS = 'manage_groups'¶
Allows group edition and creation.
- MANAGE_OIDC = 'manage_oidc'¶
Allows OpenID Connect client managements.
- MANAGE_USERS = 'manage_users'¶
Allows other users management.
- USE_OIDC = 'use_oidc'¶
Allows OpenID Connect authentication.
- canaille.oidc.configuration.OIDCSettings[source]¶
OpenID Connect settings.
Belong in the
CANAILLE_OIDC
namespace.- DYNAMIC_CLIENT_REGISTRATION_OPEN: bool = False¶
Whether a token is needed for the RFC7591 dynamical client registration.
If
True
, no token is needed to register a client. IfFalse
, dynamical client registration needs a token defined inDYNAMIC_CLIENT_REGISTRATION_TOKENS
.
- DYNAMIC_CLIENT_REGISTRATION_TOKENS: list[str] | None = None¶
A list of tokens that can be used for dynamic client registration.
- JWT: JWTSettings = JWTSettings(PRIVATE_KEY=None, PUBLIC_KEY=None, ISS=None, KTY='RSA', ALG='RS256', EXP=3600, MAPPING=JWTMappingSettings(SUB='{{ user.user_name }}', NAME='{% if user.formatted_name %}{{ user.formatted_name }}{% endif %}', PHONE_NUMBER='{% if user.phone_numbers %}{{ user.phone_numbers[0] }}{% endif %}', EMAIL='{% if user.preferred_email %}{{ user.preferred_email }}{% endif %}', GIVEN_NAME='{% if user.given_name %}{{ user.given_name }}{% endif %}', FAMILY_NAME='{% if user.family_name %}{{ user.family_name }}{% endif %}', PREFERRED_USERNAME='{% if user.display_name %}{{ user.display_name }}{% endif %}', LOCALE='{% if user.preferred_language %}{{ user.preferred_language }}{% endif %}', ADDRESS='{% if user.formatted_address %}{{ user.formatted_address }}{% endif %}', PICTURE="{% if user.photo %}{{ url_for('core.account.photo', user=user, field='photo', _external=True) }}{% endif %}", WEBSITE='{% if user.profile_url %}{{ user.profile_url }}{% endif %}'))¶
JSON Web Token settings.
- canaille.oidc.configuration.JWTSettings[source]¶
JSON Web Token settings. Belong in the
CANAILLE_OIDC.JWT
namespace.You can generate a RSA keypair with:
openssl genrsa -out private.pem 4096 openssl rsa -in private.pem -pubout -outform PEM -out public.pem
- MAPPING: JWTMappingSettings | None = JWTMappingSettings(SUB='{{ user.user_name }}', NAME='{% if user.formatted_name %}{{ user.formatted_name }}{% endif %}', PHONE_NUMBER='{% if user.phone_numbers %}{{ user.phone_numbers[0] }}{% endif %}', EMAIL='{% if user.preferred_email %}{{ user.preferred_email }}{% endif %}', GIVEN_NAME='{% if user.given_name %}{{ user.given_name }}{% endif %}', FAMILY_NAME='{% if user.family_name %}{{ user.family_name }}{% endif %}', PREFERRED_USERNAME='{% if user.display_name %}{{ user.display_name }}{% endif %}', LOCALE='{% if user.preferred_language %}{{ user.preferred_language }}{% endif %}', ADDRESS='{% if user.formatted_address %}{{ user.formatted_address }}{% endif %}', PICTURE="{% if user.photo %}{{ url_for('core.account.photo', user=user, field='photo', _external=True) }}{% endif %}", WEBSITE='{% if user.profile_url %}{{ user.profile_url }}{% endif %}')¶
- canaille.oidc.configuration.JWTMappingSettings[source]¶
Mapping between the user model and the JWT fields.
Fields are evaluated with jinja. A
user
var is available.
- canaille.backends.sql.configuration.SQLSettings[source]¶
Settings related to the SQL backend.
Belong in the
CANAILLE_SQL
namespace.
- canaille.backends.ldap.configuration.LDAPSettings[source]¶
Settings related to the LDAP backend.
Belong in the
CANAILLE_LDAP
namespace.- GROUP_BASE: str [Required]¶
The LDAP node under which groups will be looked for and saved.
For instance “ou=groups,dc=mydomain,dc=tld”.
- USER_BASE: str [Required]¶
The LDAP node under which users will be looked for and saved.
For instance ou=users,dc=mydomain,dc=tld.
Example file¶
Here is a configuration file example:
# The Flask secret key for cookies. You MUST change this.
SECRET_KEY = "change me before you go in production"
# The interface on which canaille will be served
# SERVER_NAME = "auth.mydomain.example"
# PREFERRED_URL_SCHEME = "https"
[CANAILLE]
# Your organization name.
# NAME = "Canaille"
# You can display a logo to be recognized on login screens
# LOGO = "/static/img/canaille-head.webp"
# Your favicon. If unset the LOGO will be used.
# FAVICON = "/static/img/canaille-c.webp"
# The name of a theme in the 'theme' directory, or a path path
# to a theme. Defaults to 'default'. Theming is done with
# https://github.com/tktech/flask-themer
# THEME = "default"
# If unset, language is detected
# LANGUAGE = "en"
# The timezone in which datetimes will be displayed to the users.
# If unset, the server timezone will be used.
# TIMEZONE = UTC
# If you have a sentry instance, you can set its dsn here:
# SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
# Enables javascript to smooth the user experience
# JAVASCRIPT = true
# Accelerates webpages with async requests
# HTMX = true
# If EMAIL_CONFIRMATION is set to true, users will need to click on a
# confirmation link sent by email when they want to add a new email.
# By default, this is true if SMTP is configured, else this is false.
# If explicitly set to true and SMTP is disabled, the email field
# will be read-only.
# EMAIL_CONFIRMATION = True
# If ENABLE_REGISTRATION is true, then users can freely create an account
# at this instance. If email verification is available, users must confirm
# their email before the account is created.
# ENABLE_REGISTRATION = false
# If HIDE_INVALID_LOGINS is set to true (the default), when a user
# tries to sign in with an invalid login, a message is shown indicating
# that the password is wrong, but does not give a clue whether the login
# exists or not.
# If HIDE_INVALID_LOGINS is set to false, when a user tries to sign in with
# an invalid login, a message is shown indicating that the login does not
# exist.
# HIDE_INVALID_LOGINS = true
# If ENABLE_PASSWORD_RECOVERY is false, then users cannot ask for a password
# recovery link by email. This option is true by default.
# ENABLE_PASSWORD_RECOVERY = true
# If ENABLE_INTRUDER_LOCKOUT is true, then users will have to wait for an
# increasingly long time between each failed login attempt. This option is false by default.
# ENABLE_INTRUDER_LOCKOUT = false
# If OTP_METHOD is defined, then users will need to authenticate themselves
# using a one-time password (OTP) via an authenticator app.
# Two options are supported : "TOTP" for time one-time password,
# and "HOTP" for HMAC-based one-time password.
# This option is deactivated by default.
# OTP_METHOD = "TOTP"
# If EMAIL_OTP is true, then users will need to authenticate themselves
# via a one-time password sent to their primary email address.
# This option is false by default.
# EMAIL_OTP = false
# If SMS_OTP is true, then users will need to authenticate themselves
# via a one-time password sent to their primary phone number.
# This option is false by default.
# SMS_OTP = false
# The validity duration of registration invitations, in seconds.
# Defaults to 2 days
# INVITATION_EXPIRATION = 172800
# LOGGING configures the logging output:
# - if unset, everything is logged in the standard output
# the log level is debug if DEBUG is True, else this is INFO
# - if this is a dictionary, it is passed to the python dictConfig method:
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
# - if this is a string, it is passed to the python fileConfig method
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
# Minimum length for user password.
# It is possible not to set a minimum, by entering None or 0.
# MIN_PASSWORD_LENGTH = 8
# Maximum length for user password.
# There is a technical limit with passlib used by sql database of 4096
# characters. If the value entered is 0 or None, or greater than 4096,
# then 4096 will be retained.
# MAX_PASSWORD_LENGTH = 1000
# Administration email contact.
# In certain special cases (example : questioning about password
# corruption), it is necessary to provide an administration contact
# email.
# ADMIN_EMAIL = "admin@mydomain.example"
# If :py:data:`True`, Canaille will check for password compromise on HIBP
# every time a new password is register.
# (https://haveibeenpwned.com/)
# ENABLE_PASSWORD_COMPROMISSION_CHECK = False
# Have i been pwned api url for compromission checks.
# This url should not be modified.
# API_URL_HIBP = "https://api.pwnedpasswords.com/range/"
# Password validity duration.
# If a value is recorded Canaille will check if user's password is expired.
# Then, the user is forced to change his password when the lifetime of the password is over.
# This value is expressed in `ISO8601 format <https://en.wikipedia.org/wiki/ISO_8601#Durations>`_.
# Example for 60 days: "P60D"
# It is possible to disable this option by entering None.
# PASSWORD_LIFETIME = None
# [CANAILLE_SQL]
# The SQL database connection string
# Details on https://docs.sqlalchemy.org/en/20/core/engines.html
# DATABASE_URI = "postgresql://user:password@localhost/database"
# [CANAILLE_LDAP]
# URI = "ldap://ldap"
# ROOT_DN = "dc=mydomain,dc=tld"
# BIND_DN = "cn=admin,dc=mydomain,dc=tld"
# BIND_PW = "admin"
# TIMEOUT =
# Where to search for users?
# USER_BASE = "ou=users,dc=mydomain,dc=tld"
# The object class to use for creating new users
# Note : If you plan on using TOTP/HOTP authentication, use : USER_CLASS = ["inetOrgPerson", "oathHOTPToken"]
# USER_CLASS = "inetOrgPerson"
# The attribute to identify an object in the User dn.
# USER_RDN = "uid"
# Filter to match users on sign in. Jinja syntax is supported
# and a `login` variable is available containing the value
# passed in the login field.
# USER_FILTER = "(|(uid={{ login }})(mail={{ login }}))"
# Where to search for groups?
# GROUP_BASE = "ou=groups,dc=mydomain,dc=tld"
# The object class to use for creating new groups
# GROUP_CLASS = "groupOfNames"
# The attribute to identify an object in the User dn.
# GROUP_RDN = "cn"
# The attribute to use to identify a group
# GROUP_NAME_ATTRIBUTE = "cn"
[CANAILLE.ACL]
# You can define access controls that define what users can do on canaille
# An access control consists in a FILTER to match users, a list of PERMISSIONS
# matched users will be able to perform, and fields users will be able
# to READ and WRITE. Users matching several filters will cumulate permissions.
#
# 'FILTER' parameter can be:
# - absent, in which case all the users will match this access control
# - a mapping where keys are user attributes name and the values those user
# attribute values. All the values must be matched for the user to be part
# of the access control.
# - a list of those mappings. If a user values match at least one mapping,
# then the user will be part of the access control
#
# Here are some examples
# FILTER = {user_name = 'admin'}
# FILTER =
# - {groups = 'admins}
# - {groups = 'moderators'}
#
# The 'PERMISSIONS' parameter that is an list of items the users in the access
# control will be able to manage. 'PERMISSIONS' is optional. Values can be:
# - "edit_self" to allow users to edit their own profile
# - "use_oidc" to allow OpenID Connect authentication
# - "manage_oidc" to allow OpenID Connect client managements
# - "manage_users" to allow other users management
# - "manage_groups" to allow group edition and creation
# - "delete_account" allows a user to delete his own account. If used with
# manage_users, the user can delete any account
# - "impersonate_users" to allow a user to take the identity of another user
#
# The 'READ' and 'WRITE' attributes are the LDAP attributes of the user
# object that users will be able to read and/or write.
[CANAILLE.ACL.DEFAULT]
PERMISSIONS = ["edit_self", "use_oidc"]
READ = [
"user_name",
"groups",
"lock_date",
]
WRITE = [
"photo",
"given_name",
"family_name",
"display_name",
"password",
"phone_numbers",
"emails",
"profile_url",
"formatted_address",
"street",
"postal_code",
"locality",
"region",
"preferred_language",
"employee_number",
"department",
"title",
"organization",
]
[CANAILLE.ACL.ADMIN]
FILTER = {groups = "admins"}
PERMISSIONS = [
"manage_users",
"manage_groups",
"manage_oidc",
"delete_account",
"impersonate_users",
]
WRITE = [
"groups",
"lock_date",
]
[CANAILLE_OIDC]
# Whether a token is needed for the RFC7591 dynamical client registration.
# If true, no token is needed to register a client.
# If false, dynamical client registration needs a token defined
# in DYNAMIC_CLIENT_REGISTRATION_TOKENS
# DYNAMIC_CLIENT_REGISTRATION_OPEN = false
# A list of tokens that can be used for dynamic client registration
# DYNAMIC_CLIENT_REGISTRATION_TOKENS = [
# "xxxxxxx-yyyyyyy-zzzzzz",
# ]
# REQUIRE_NONCE force the nonce exchange during the authentication flows.
# This adds security but may not be supported by all clients.
# REQUIRE_NONCE = true
[CANAILLE_OIDC.JWT]
# PRIVATE_KEY and PUBLIC_KEY are the private and
# the public key. You can generate a RSA keypair with:
# openssl genrsa -out private.pem 4096
# openssl rsa -in private.pem -pubout -outform PEM -out public.pem
# If the variables are unset, and debug mode is enabled,
# a in-memory keypair will be used.
# PRIVATE_KEY = "..."
# PUBLIC_KEY = "..."
# The URI of the identity provider
# ISS = "https://auth.mydomain.example"
# The key type parameter
# KTY = "RSA"
# The key algorithm
# ALG = "RS256"
# The time the JWT will be valid, in seconds
# EXP = 3600
[CANAILLE_OIDC.JWT.MAPPING]
# Mapping between JWT fields and LDAP attributes from your
# User objectClass.
# {attribute} will be replaced by the user ldap attribute value.
# Default values fits inetOrgPerson.
# SUB = "{{ user.user_name }}"
# NAME = "{{ user.formatted_name }}"
# PHONE_NUMBER = "{{ user.phone_numbers[0] }}"
# EMAIL = "{{ user.preferred_email }}"
# GIVEN_NAME = "{{ user.given_name }}"
# FAMILY_NAME = "{{ user.family_name }}"
# PREFERRED_USERNAME = "{{ user.display_name }}"
# LOCALE = "{{ user.preferred_language }}"
# ADDRESS = "{{ user.formatted_address }}"
# PICTURE = "{% if user.photo %}{{ url_for('core.account.photo', user=user, field='photo', _external=True) }}{% endif %}"
# WEBSITE = "{{ user.profile_url }}"
# The SMTP server options. If not set, mail related features such as
# user invitations, and password reset emails, will be disabled.
[CANAILLE.SMTP]
# HOST = "localhost"
# PORT = 25
# TLS = false
# SSL = false
# LOGIN = ""
# PASSWORD = ""
# FROM_ADDR = "admin@mydomain.example"
# The SMPP server options. If not set, sms related features such as
# sms one-time passwords will be disabled.
[CANAILLE.SMPP]
# HOST = "localhost"
# PORT = 2775
# LOGIN = ""
# PASSWORD = ""