From 7830523447f414428475dd2d582a3bbcd60c484f Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 6 Mar 2026 15:08:02 -0500 Subject: [PATCH] user: add required birth date field to user creation Add a required birthDate prompt (YYYY-MM-DD) during user creation. The date is validated (format, not future, year >= 1900) and stored as a systemd userdb JSON drop-in in /etc/userdb/.user on the target system. This integrates with systemd's JSON user record birthDate field. --- archinstall/lib/installer.py | 15 +++++++++++++ archinstall/lib/models/users.py | 11 ++++++++- archinstall/lib/user/user_menu.py | 37 ++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 9e4da9dc79..bb4e3f2f1f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,4 +1,5 @@ import glob +import json import os import platform import re @@ -1943,6 +1944,20 @@ def _create_user(self, user: User) -> None: if user.sudo: self.enable_sudo(user) + if user.birth_date: + self._write_userdb_dropin(user) + + def _write_userdb_dropin(self, user: User) -> None: + userdb_dir = self.target / 'etc' / 'userdb' + userdb_dir.mkdir(parents=True, exist_ok=True) + + dropin = userdb_dir / f'{user.username}.user' + record = {'userName': user.username, 'birthDate': user.birth_date} + dropin.write_text(json.dumps(record)) + dropin.chmod(0o644) + + info(f'Wrote userdb drop-in for {user.username} with birth date') + def set_user_password(self, user: User) -> bool: info(f'Setting password for {user.username}') diff --git a/archinstall/lib/models/users.py b/archinstall/lib/models/users.py index 8fda6888ff..44e9e9f07a 100644 --- a/archinstall/lib/models/users.py +++ b/archinstall/lib/models/users.py @@ -107,6 +107,7 @@ def _check_password_strength( 'sudo': bool, 'groups': list[str], 'enc_password': str | None, + 'birth_date': NotRequired[str], }, ) @@ -158,6 +159,7 @@ class User: password: Password sudo: bool groups: list[str] = field(default_factory=list) + birth_date: str = '' @override def __str__(self) -> str: @@ -170,16 +172,22 @@ def table_data(self) -> dict[str, str | bool | list[str]]: 'password': self.password.hidden(), 'sudo': self.sudo, 'groups': self.groups, + 'birth_date': self.birth_date, } def json(self) -> UserSerialization: - return { + data: UserSerialization = { 'username': self.username, 'enc_password': self.password.enc_password, 'sudo': self.sudo, 'groups': self.groups, } + if self.birth_date: + data['birth_date'] = self.birth_date + + return data + @classmethod def parse_arguments( cls, @@ -208,6 +216,7 @@ def parse_arguments( password=password, sudo=entry.get('sudo', False) is True, groups=groups, + birth_date=entry.get('birth_date', ''), ) users.append(user) diff --git a/archinstall/lib/user/user_menu.py b/archinstall/lib/user/user_menu.py index 3f50f517f7..29c0fa8518 100644 --- a/archinstall/lib/user/user_menu.py +++ b/archinstall/lib/user/user_menu.py @@ -1,4 +1,5 @@ import re +from datetime import UTC, date, datetime from typing import override from archinstall.lib.menu.helpers import Confirmation, Input @@ -91,6 +92,12 @@ def _add_user(self) -> User | None: return None header += f'{tr("Password")}: {password.hidden()}\n' + + birth_date = self._ask_birth_date(header) + + if birth_date: + header += f'{tr("Birth date")}: {birth_date}\n' + prompt = f'{header}\n' + tr('Should "{}" be a superuser (sudo)?\n').format(username) result = Confirmation( @@ -105,7 +112,35 @@ def _add_user(self) -> User | None: case _: raise ValueError('Unhandled result type') - return User(username, password, sudo) + return User(username, password, sudo, birth_date=birth_date) + + def _validate_birth_date(self, value: str | None) -> str | None: + if not value: + return tr('Birth date is required') + try: + dt = date.fromisoformat(value) + except ValueError: + return tr('Invalid date format. Use YYYY-MM-DD') + if dt > datetime.now(tz=UTC).date(): + return tr('Birth date cannot be in the future') + if dt.year < 1900: + return tr('Birth date year must be 1900 or later') + return None + + def _ask_birth_date(self, header: str) -> str: + prompt = f'{header}\n' + tr('Enter birth date (YYYY-MM-DD, e.g. 2000-01-01)') + + result = Input( + prompt, + allow_skip=False, + validator_callback=self._validate_birth_date, + ).show() + + match result.type_: + case ResultType.Selection: + return result.get_value() or '' + case _: + raise ValueError('Unhandled result type') def select_users(prompt: str = '', preset: list[User] = []) -> list[User]: