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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ This MyTraL **minor** release brings:
- .

### Added

- Improved the first login experience in MyTraL DESKTOP incarnation: when no
user profile exists yet, a default `athlete` user ("MyTraL Athlete") is
auto-created and auto-logged in, landing straight on the dashboard - no
sign-up/login step required. Logging out disables auto-login until the next
explicit login, so a second/third user can be added or logged into from the
login page.
- Calendar and sickness heatmaps now mark today with a yellow rectangle so the
current week and day stand out, and each sickness day's tooltip now reads like
`2026-05-25: 3x sick` instead of a bare count.

### Fixed
- .
- Fixed path(s) in Snap which pointed outside of strict confinement filesystem.

### Documentation
- .
Expand Down
9 changes: 8 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ This MyTraL **minor** release brings:
- .

### Added

- Improved the first login experience in MyTraL DESKTOP incarnation: when no
user profile exists yet, a default `athlete` user ("MyTraL Athlete") is
auto-created and auto-logged in, landing straight on the dashboard - no
sign-up/login step required. Logging out disables auto-login until the next
explicit login, so a second/third user can be added or logged into from the
login page.
- Calendar and sickness heatmaps now mark today with a yellow rectangle so the
current week and day stand out, and each sickness day's tooltip now reads like
`2026-05-25: 3x sick` instead of a bare count.

### Fixed
- .
- Fixed path(s) in Snap which pointed outside of strict confinement filesystem.

### Documentation
- .
Expand Down
27 changes: 3 additions & 24 deletions docs/INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,31 +108,10 @@ The easiest way to install MyTraL on Linux is from the
distributions and keeps MyTraL up to date automatically.
This package uses **strict confinement** and is published straight to the Snap Store.

Install `Snapd` (if not already installed):
Install `Snapd` (if not already installed) by following the official guide for
your distribution:

**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install snapd
```

**Fedora:**
```bash
sudo dnf install snapd
sudo ln -s /var/lib/snapd/snap /snap
```

**Arch Linux:**
```bash
sudo pacman -S snapd
sudo systemctl enable --now snapd.socket
```

**openSUSE:**
```bash
sudo zypper install snapd
sudo systemctl enable --now snapd
```
* [Installing snapd](https://snapcraft.io/docs/tutorials/install-the-daemon/)

Install MyTraL from the Snap Store:

Expand Down
7 changes: 7 additions & 0 deletions mytral/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""MyTraL main module: configure and start the web application."""

from mytral import blobstore as _blobstore_pkg
from mytral import bootstraps
from mytral import config
from mytral import loggers
from mytral import releng
Expand Down Expand Up @@ -62,6 +63,12 @@
app_ds = dataset.MyTraLDataset(mytral_config=app_config, logger=app_logger)
# MyTraL user dataset: persistence agnostic access to the user data
app_user_ds = app_ds.user()

# smooth first start: auto-create the default athlete user on desktop
# installations which don't have any user profile yet
if app_config.incarnation == config.MytralIncarnation.DESKTOP:
bootstraps.bootstrap_default_desktop_user(ds=app_user_ds, logger=app_logger)

# blob store for activity attachments (GPX files and photos)
app_blobstore = _blobstore_pkg.create_blobstore(app_config)

Expand Down
7 changes: 7 additions & 0 deletions mytral/backends/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ def register_new_user(
self,
user_name: str = commons.DEFAULT_USER_NAME,
user_id: str = "",
user_display_name: str = "",
password_enc: str = "",
auto_login: bool = False,
):
"""Create new user (sandbox / entries / tables).

Expand All @@ -104,8 +106,13 @@ def register_new_user(
Username.
user_id : str
Unique user ID.
user_display_name : str
Display name shown in the UI.
password_enc : str
Encrypted password.
auto_login : bool
Whether the new profile should have auto-login enabled
(DESKTOP incarnation only).

"""
raise NotImplementedError
Expand Down
2 changes: 2 additions & 0 deletions mytral/backends/datasets/dataset_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ def register_new_user(
user_id: str = "",
user_display_name: str = "",
password_enc: str = "",
auto_login: bool = False,
):
self.user_dir(user_id)

Expand All @@ -1100,6 +1101,7 @@ def register_new_user(
born_day=commons.BOOTSTRAP_BORN_DAY,
height=commons.BOOTSTRAP_HEIGHT_CM,
gender=True,
auto_login=auto_login,
)
)

Expand Down
111 changes: 81 additions & 30 deletions mytral/blueprints/auth_uri_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from mytral import security
from mytral import settings as user_settings
from mytral.migrations import FsPersistenceMigrations
from mytral.routes import COOKIE_AUTO_LOGIN_SUPPRESSED
from mytral.routes import COOKIE_MOBILE
from mytral.routes import COOKIE_TOKEN
from mytral.routes import COOKIE_USER
Expand Down Expand Up @@ -161,7 +162,9 @@ def signup():

# log user in - store user_id (UUID) not username
flask.session[COOKIE_USER] = user_id
flask.session[COOKIE_TOKEN] = uuid.uuid4()
flask.session[COOKIE_TOKEN] = str(uuid.uuid4())
# a fresh, explicit login re-enables auto-login for the next boot
flask.session.pop(COOKIE_AUTO_LOGIN_SUPPRESSED, None)

flask.flash(
message=f"Welcome {new_username}! Your account has been created.",
Expand All @@ -183,6 +186,46 @@ def signup():
return flask.redirect(flask.url_for("signup"))


def _run_fs_migration() -> bool:
"""Run any pending filesystem data migration.

Reads the current data spec version from disk (bypassing the cache),
runs any applicable migration steps and persists the new data spec
version to config.json.

Returns
-------
bool :
True if the data is at the current spec afterwards (migration
succeeded or none was needed), False if a migration was attempted
but failed.
"""
# force a fresh read from disk (bypass cache)
config.MytralPersistenceFsConfig.invalidate_cache()
cfg = config.MytralPersistenceFsConfig(app_config)

if not cfg.is_migrate():
return True

try:
migrations = FsPersistenceMigrations(
logger=app_logger, cfg=cfg, ds=mytral.app_ds
)
migrations.migrate()

# persist the updated data spec version to config.json
cfg.update_data_spec_version()

# invalidate cache so the next disk read sees the updated state
config.MytralPersistenceFsConfig.invalidate_cache()
return True
except Exception as ex:
app_logger.error(
f"Data migration failed: {ex}", traceback=traceback.format_exc()
)
return False


@flask_app.route("/login", methods=["GET", "POST"])
def login():
# MyTraL (access) token is generated on login by MyTraL server and stored
Expand All @@ -204,7 +247,35 @@ def login():

auto_login_usernames = []
if app_config.incarnation == config.MytralIncarnation.DESKTOP:
auto_login_usernames = list(ds.list_profile_names(auto_login=True).keys())
auto_login_profiles = ds.list_profile_names(auto_login=True)
auto_login_usernames = list(auto_login_profiles.keys())

# smooth first start / single-user desktop: silently log in as the
# sole auto-login user, unless the user just logged out on purpose
if len(auto_login_profiles) == 1 and not flask.session.get(
COOKIE_AUTO_LOGIN_SUPPRESSED, False
):
Comment thread
dvorka marked this conversation as resolved.
# a pending data migration must run before we log the sole
# user in - keep it non-interactive to preserve the smooth
# desktop start, but never proceed on stale data if it fails
if _run_fs_migration():
user_name, user_id = next(iter(auto_login_profiles.items()))

flask.session[COOKIE_USER] = user_id
flask.session[COOKIE_TOKEN] = str(uuid.uuid4())

flask.flash(
message=f"Auto logged in as user '{user_name}'",
category="success",
)
return flask.redirect(flask.url_for("home"))

# migration failed: fall through to the login page, which
# surfaces the migration UI and the error to the user
flask.flash(
message="Data migration failed. Please check the logs.",
category="error",
)

return flask.render_template(
"log-in.html",
Expand Down Expand Up @@ -294,6 +365,8 @@ def login():
flask.session[COOKIE_USER] = user_id
# safe foo token to client cookies
flask.session[COOKIE_TOKEN] = str(uuid.uuid4())
# a fresh, explicit login re-enables auto-login for the next boot
flask.session.pop(COOKIE_AUTO_LOGIN_SUPPRESSED, None)

# if the page width is <800px, then switch to mobile view
app_logger.debug(f"Storing page width to session: {form.page_width.data}")
Expand All @@ -317,39 +390,14 @@ def login_migrate():
migration steps, updates config.json with the new version, and
redirects back to the login page.
"""
# force a fresh read from disk (bypass cache)
config.MytralPersistenceFsConfig.invalidate_cache()
cfg = config.MytralPersistenceFsConfig(app_config)

if not cfg.is_migrate():
flask.flash(
message="No data migration is needed at this time.",
category="info",
)
return flask.redirect(flask.url_for("login"))

try:
migrations = FsPersistenceMigrations(
logger=app_logger, cfg=cfg, ds=mytral.app_ds
)
migrations.migrate()

# persist the updated data spec version to config.json
cfg.update_data_spec_version()

# invalidate cache so the next login page GET reads the updated disk state
config.MytralPersistenceFsConfig.invalidate_cache()

if _run_fs_migration():
flask.flash(
message="Data migration completed successfully. You can now log in.",
category="success",
)
except Exception as ex:
app_logger.error(
f"Data migration failed: {ex}", traceback=traceback.format_exc()
)
else:
flask.flash(
message=f"Data migration failed: {ex}. Please check the logs.",
message="Data migration failed. Please check the logs.",
category="error",
)

Expand All @@ -362,6 +410,9 @@ def logout():
flask.session.pop(COOKIE_TOKEN, None)
if user_id:
ds.cache_evict(user_id=user_id)
# desktop: suppress auto-login so the user can pick/create another account
if app_config.incarnation == config.MytralIncarnation.DESKTOP:
flask.session[COOKIE_AUTO_LOGIN_SUPPRESSED] = True
return flask.redirect(flask.url_for("login"))


Expand Down
44 changes: 44 additions & 0 deletions mytral/bootstraps.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,47 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Bootstrap related code."""

import uuid

from mytral import commons
from mytral import loggers
from mytral import security
from mytral.backends import dataset


def bootstrap_default_desktop_user(
ds: dataset.UserDataset, logger: loggers.MytralStructLogger
) -> None:
"""Auto-create the default athlete user on the first DESKTOP boot.

Smooth first start: DESKTOP incarnation with no user profile on disk yet
gets a default user (with auto-login enabled) so that it can go straight
to the dashboard, without sign-up/login. Installations which already have
at least one profile are left untouched.

Parameters
----------
ds : dataset.UserDataset
User dataset used to check for existing profiles and register the
default user.
logger : loggers.MytralStructLogger
Logger used to record that the default user was created.

"""
if ds.list_profiles():
return

user_id = str(uuid.uuid4())
ds.register_new_user(
user_name=commons.DEFAULT_DESKTOP_USER_NAME,
user_id=user_id,
user_display_name=commons.DEFAULT_DESKTOP_USER_DISPLAY_NAME,
password_enc=security.hash_password(commons.DEFAULT_DESKTOP_USER_PASSWORD),
auto_login=True,
)
Comment thread
dvorka marked this conversation as resolved.
logger.info(
"Bootstrapped default desktop user",
user_name=commons.DEFAULT_DESKTOP_USER_NAME,
user_id=user_id,
)
7 changes: 6 additions & 1 deletion mytral/commons.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@
# main applications dataset name
DATASET_NAME_MAIN = "lifelong"

# default user name (desktop app installation)
# default user name (tests, development) - IMPROVE: refactor to "athlete"
DEFAULT_USER_NAME = "dvorka"

# default user auto-created on the first DESKTOP boot when no user exists yet
DEFAULT_DESKTOP_USER_NAME = "athlete"
DEFAULT_DESKTOP_USER_DISPLAY_NAME = "MyTraL Athlete"
DEFAULT_DESKTOP_USER_PASSWORD = "changeit"
Comment thread
dvorka marked this conversation as resolved.

Comment thread
dvorka marked this conversation as resolved.
#
# DATASETS & PERSISTENCE
#
Expand Down
4 changes: 4 additions & 0 deletions mytral/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
# the resolution is detected on login and stored in session the cookie,
# empty value means that the user is not using mobile device
COOKIE_MOBILE = "mytral_mobile"
# desktop only: set on logout to suppress the automatic re-login on the
# next /login page load, so the user can pick/create another account;
# cleared again on the next successful login/sign-up
COOKIE_AUTO_LOGIN_SUPPRESSED = "mytral_auto_login_suppressed"
# aspects
ASPECT_LIST = "list"
ASPECT_WEIGHT = "weight"
Expand Down
7 changes: 5 additions & 2 deletions mytral/run_desktop.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@
so that the init steps (which would be potentially unsafe for webapp) can be done.
This env var / setting cannot be changed on desktop.
- DEFAULT USER
- User ``mytral`` / ``mytral`` w/ auto login enabled is auto created on
the first boot so that the desktop application can AUTO LOGIN as that user.
- User ``athlete`` (display name "MyTraL Athlete") w/ auto login enabled
is auto created on the first boot - only when no user profile exists
yet - so that the desktop application can AUTO LOGIN as that user.
- SECURITY: user can change default user password
- SECURITY: user can disable auto login.
- SECURITY: user can create new / other users.
- SECURITY: logging out disables auto login until the next explicit login,
so that user can add a new account(s).
- AUTO LOGIN
- If user logs-in with username (account) which has enabled auto login,
then the password is not checked and user is let in. This flag can be set
Expand Down
Loading
Loading