Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/hosting/custom_branding.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,27 @@ You can also customize some specific texts in Parsec. To do so, simply:

.. literalinclude:: custom_fr-FR.json
:language: json

Server emails & HTML pages
--------------------------

Parsec server emails and HTML pages (index, 404) are based on the `Jinja template syntax`_.

.. _Jinja template syntax: https://jinja.palletsprojects.com/en/stable/templates/

You can customize them by providing a custom template directory when running the server with ``--template-dir`` or by setting the environment variable ``PARSEC_TEMPLATE_DIR``.

The directory should contain the following files:

- ``index.html``: default landing page when you access the server.
- ``404.html``: resource not found page.
- ``email/account_create.[html|txt].j2``: HTML and TEXT templates for the email send to confirm Parsec account creation.
- ``email/account_delete.[html|txt].j2``: HTML and TEXT email templates to confirm Parsec account deletion.
- ``email/account_recover.[html|txt].j2``: HTML and TEXT email templates to confirm Parsec account recovery.
- ``email/invitation.[html|txt].j2``: HTML and TEXT email templates to send invitation to join an organization.

.. note::

You can base your customization on the default server's templates `found here <parsec-server-template-src_>`_.

.. _parsec-server-template-src: https://github.com/Scille/parsec-cloud/tree/v3.4.1-a.0+dev/server/parsec/templates

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note (non-blocking): The link does not work as version 3.4.1-a.0+dev is not released

77 changes: 76 additions & 1 deletion docs/locale/fr/LC_MESSAGES/hosting/custom_branding.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Parsec 3.4.1-a.0+dev\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-07 09:23+0200\n"
"POT-Creation-Date: 2025-07-25 06:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n"
Expand Down Expand Up @@ -187,3 +187,78 @@ msgstr "custom_en-US.json"
#: ../../hosting/custom_branding.rst:61
msgid "custom_fr-FR.json"
msgstr "custom_fr-FR.json"

#: ../../hosting/custom_branding.rst:67
msgid "Server emails & HTML pages"
msgstr "Emails et pages HTML du serveur"

#: ../../hosting/custom_branding.rst:69
msgid ""
"Parsec server emails and HTML pages (index, 404) are based on the `Jinja "
"template syntax`_."
msgstr ""
"Les courriels du serveur Parsec et les pages HTML (index, 404) sont basés "
"sur `la syntaxe de template Jinja <Jinja template syntax_>`_."

#: ../../hosting/custom_branding.rst:73
msgid ""
"You can customize them by providing a custom template directory when running "
"the server with ``--template-dir`` or by setting the environment variable "
"``PARSEC_TEMPLATE_DIR``."
msgstr ""
"Vous pouvez les personnaliser en fournissant un répertoire de modèles "
"personnalisé lors de l'exécution du serveur avec ``--template-dir`` ou en "
"définissant la variable d'environnement ``PARSEC_TEMPLATE_DIR``."

#: ../../hosting/custom_branding.rst:75
msgid "The directory should contain the following files:"
msgstr "Le dossier doit contenir les fichiers suivants :"

#: ../../hosting/custom_branding.rst:77
msgid "``index.html``: default landing page when you access the server."
msgstr ""
"``index.html`` : page d'accueil par défaut lorsque vous accédez au serveur."

#: ../../hosting/custom_branding.rst:78
msgid "``404.html``: resource not found page."
msgstr "``404.html`` : page de ressource non trouvée."

#: ../../hosting/custom_branding.rst:79
msgid ""
"``email/account_create.[html|txt].j2``: HTML and TEXT templates for the "
"email send to confirm Parsec account creation."
msgstr ""
"``email/account_create.[html|txt].j2`` : Modèles HTML et TEXTE pour le "
"courriel envoyé afin de confirmer la création d'un compte Parsec."

#: ../../hosting/custom_branding.rst:80
msgid ""
"``email/account_delete.[html|txt].j2``: HTML and TEXT email templates to "
"confirm Parsec account deletion."
msgstr ""
"``email/account_delete.[html|txt].j2``: Modèles d'e-mail HTML et TEXTE pour "
"confirmer la suppression du compte Parsec."

#: ../../hosting/custom_branding.rst:81
msgid ""
"``email/account_recover.[html|txt].j2``: HTML and TEXT email templates to "
"confirm Parsec account recovery."
msgstr ""
"``email/account_recover.[html|txt].j2`` : modèles d'e-mails HTML et TEXTE "
"pour confirmer la récupération du compte Parsec."

#: ../../hosting/custom_branding.rst:82
msgid ""
"``email/invitation.[html|txt].j2``: HTML and TEXT email templates to send "
"invitation to join an organization."
msgstr ""
"``email/invitation.[html|txt].j2`` : modèles d'e-mails HTML et TEXTE pour "
"envoyer une invitation à rejoindre une organisation."

#: ../../hosting/custom_branding.rst:86
msgid ""
"You can base your customization on the default server's templates `found "
"here <parsec-server-template-src_>`_."
msgstr ""
"Vous pouvez baser votre personnalisation sur les modèles du serveur par "
"défaut `disponibles ici <parsec-server-template-src_>`_."
5 changes: 5 additions & 0 deletions misc/version_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ def _only_major_version(version: str) -> str:
r"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh -s -- -y --default-toolchain [0-9.]+",
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain {version}",
)
PARSEC_REPO_SRC_BASE_URL = ReplaceRegex(
r"github.com/Scille/parsec-cloud/tree/v.*?/",
"github.com/Scille/parsec-cloud/tree/v{version}/",
)


@enum.unique
Expand Down Expand Up @@ -366,6 +370,7 @@ def set_tool_version(tool: Tool, version: str) -> None:
)
],
},
ROOT_DIR / "docs/hosting/custom_branding.rst": {Tool.Parsec: [PARSEC_REPO_SRC_BASE_URL]},
ROOT_DIR / "docs/hosting/install_cli.rst": {
Tool.Parsec: [
ReplaceRegex(
Expand Down
8 changes: 2 additions & 6 deletions server/parsec/asgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,11 @@
from parsec.asgi.administration import administration_router
from parsec.asgi.redirect import redirect_router
from parsec.asgi.rpc import Backend, rpc_router
from parsec.templates import JINJA_ENV_CONFIG

logger = get_logger()

type AsgiApp = FastAPI


templates = Jinja2Templates(
directory=(Path(__file__) / "../../templates").resolve(), **JINJA_ENV_CONFIG
)

tags_metadata = [
{
"name": "administration",
Expand Down Expand Up @@ -81,6 +75,8 @@ def app_factory(
)
app.state.backend = backend

templates = Jinja2Templates(env=backend.config.jinja_env)

if with_client_web_app:

def root(request: Request) -> Response:
Expand Down
15 changes: 15 additions & 0 deletions server/parsec/cli/export_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
_generate_account_delete_validation_email,
)
from parsec.components.invite import generate_invite_email
from parsec.templates import get_environment

DEFAULT_SENDER_EMAIL = EmailAddress("parsec@example.com")
DEFAULT_RECIPIENT_EMAIL = EmailAddress("alice@example.com")
Expand Down Expand Up @@ -82,6 +83,11 @@ def mail_templates_shared_options[**P, T](fn: Callable[P, T]) -> Callable[P, T]:
show_default=True,
help="The base server url used to access static resources",
),
click.option(
"--template-dir",
type=click.Path(dir_okay=True, file_okay=False, exists=True, path_type=Path),
help="Load templates from the specified directory instead of using the default one",
),
]
for decorator in decorators:
fn = decorator(fn)
Expand Down Expand Up @@ -123,8 +129,11 @@ def invite(
reply_to: EmailAddress | None,
server_url: str,
output_dir: Path,
template_dir: Path | None,
):
jinja_env = get_environment(template_dir)
message = generate_invite_email(
jinja_env=jinja_env,
from_addr=sender,
to_addr=recipient,
invitation_type=invitation_type,
Expand Down Expand Up @@ -152,8 +161,11 @@ def account_create(
validation_code: ValidationCode,
server_url: str,
output_dir: Path,
template_dir: Path | None,
):
jinja_env = get_environment(template_dir)
message = _generate_account_create_validation_email(
jinja_env=jinja_env,
from_addr=sender,
to_addr=recipient,
validation_code=validation_code,
Expand All @@ -177,8 +189,11 @@ def account_delete(
validation_code: ValidationCode,
server_url: str,
output_dir: Path,
template_dir: Path | None,
):
jinja_env = get_environment(template_dir)
message = _generate_account_delete_validation_email(
jinja_env=jinja_env,
from_addr=sender,
to_addr=recipient,
validation_code=validation_code,
Expand Down
11 changes: 11 additions & 0 deletions server/parsec/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
SmtpEmailConfig,
)
from parsec.logging import get_logger
from parsec.templates import get_environment

logger = get_logger()

Expand Down Expand Up @@ -300,6 +301,12 @@ def handle_parse_result(
type=ParsecAddr.from_url,
help="URL to reach this server (typically used in invitation emails)",
)
@click.option(
"--template-dir",
type=click.Path(dir_okay=True, file_okay=False, exists=True, path_type=Path),
help="Load templates from the specified directory instead of using the default one",
envvar="PARSEC_TEMPLATE_DIR",
)
@click.option(
"--email-host",
envvar="PARSEC_EMAIL_HOST",
Expand Down Expand Up @@ -470,6 +477,7 @@ def run_cmd(
validation_email_rate_limit: tuple[int, int],
fake_account_password_algorithm_seed: SecretKey,
server_addr: ParsecAddr,
template_dir: Path | None,
email_host: str,
email_port: int,
email_host_user: str | None,
Expand Down Expand Up @@ -514,7 +522,10 @@ def run_cmd(
)
logger.debug("Email config", config=email_config)

jinja_env = get_environment(template_dir)

app_config = BackendConfig(
jinja_env=jinja_env,
administration_token=administration_token,
db_config=db,
sse_keepalive=sse_keepalive,
Expand Down
4 changes: 4 additions & 0 deletions server/parsec/cli/testbed.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
SecretKey,
ValidationCode,
)
from parsec.templates import get_environment

try:
from parsec._parsec import testbed
Expand Down Expand Up @@ -360,7 +361,10 @@ async def testbed_backend_factory(
url=with_postgresql, min_connections=1, max_connections=5
)

jinja_env = get_environment(None)

config = BackendConfig(
jinja_env=jinja_env,
debug=True,
db_config=db_config,
sse_keepalive=30,
Expand Down
21 changes: 14 additions & 7 deletions server/parsec/components/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from enum import auto
from typing import Literal

from jinja2 import Environment

from parsec._parsec import (
AccountAuthMethodID,
DateTime,
Expand All @@ -28,7 +30,6 @@
from parsec.client_context import AnonymousAccountClientContext, AuthenticatedAccountClientContext
from parsec.components.email import SendEmailBadOutcome, send_email
from parsec.config import BackendConfig
from parsec.templates import get_template
from parsec.types import BadOutcomeEnum


Expand Down Expand Up @@ -171,6 +172,7 @@ async def _send_account_create_validation_email(
return SendEmailBadOutcome.BAD_SMTP_CONFIG

message = _generate_account_create_validation_email(
jinja_env=self._config.jinja_env,
from_addr=self._config.email_config.sender,
to_addr=email,
validation_code=validation_code,
Expand All @@ -193,6 +195,7 @@ async def _send_account_delete_validation_email(
return SendEmailBadOutcome.BAD_SMTP_CONFIG

message = _generate_account_delete_validation_email(
jinja_env=self._config.jinja_env,
from_addr=self._config.email_config.sender,
to_addr=email,
validation_code=validation_code,
Expand All @@ -213,6 +216,7 @@ async def _send_account_recover_validation_email(
return SendEmailBadOutcome.BAD_SMTP_CONFIG

message = _generate_account_recover_validation_email(
jinja_env=self._config.jinja_env,
from_addr=self._config.email_config.sender,
to_addr=email,
validation_code=validation_code,
Expand Down Expand Up @@ -727,6 +731,7 @@ async def api_invite_self_list(


def _generate_account_create_validation_email(
jinja_env: Environment,
from_addr: EmailAddress,
to_addr: EmailAddress,
validation_code: ValidationCode,
Expand All @@ -735,11 +740,11 @@ def _generate_account_create_validation_email(
# Quick fix to have a similar behavior between Rust and Python
server_url = server_url.removesuffix("/")

html = get_template("email/account_create.html.j2").render(
html = jinja_env.get_template("email/account_create.html.j2").render(
validation_code=validation_code.str,
server_url=server_url,
)
text = get_template("email/account_create.txt.j2").render(
text = jinja_env.get_template("email/account_create.txt.j2").render(
validation_code=validation_code.str,
server_url=server_url,
)
Expand All @@ -764,6 +769,7 @@ def _generate_account_create_validation_email(


def _generate_account_delete_validation_email(
jinja_env: Environment,
from_addr: EmailAddress,
to_addr: EmailAddress,
validation_code: ValidationCode,
Expand All @@ -772,11 +778,11 @@ def _generate_account_delete_validation_email(
# Quick fix to have a similar behavior between Rust and Python
server_url = server_url.removesuffix("/")

html = get_template("email/account_delete.html.j2").render(
html = jinja_env.get_template("email/account_delete.html.j2").render(
validation_code=validation_code.str,
server_url=server_url,
)
text = get_template("email/account_delete.txt.j2").render(
text = jinja_env.get_template("email/account_delete.txt.j2").render(
validation_code=validation_code.str,
server_url=server_url,
)
Expand All @@ -801,6 +807,7 @@ def _generate_account_delete_validation_email(


def _generate_account_recover_validation_email(
jinja_env: Environment,
from_addr: EmailAddress,
to_addr: EmailAddress,
validation_code: ValidationCode,
Expand All @@ -809,11 +816,11 @@ def _generate_account_recover_validation_email(
# Quick fix to have a similar behavior between Rust and Python
server_url = server_url.removesuffix("/")

html = get_template("email/account_recover.html.j2").render(
html = jinja_env.get_template("email/account_recover.html.j2").render(
validation_code=validation_code.str,
server_url=server_url,
)
text = get_template("email/account_recover.txt.j2").render(
text = jinja_env.get_template("email/account_recover.txt.j2").render(
validation_code=validation_code.str,
server_url=server_url,
)
Expand Down
Loading