Sunil Mohan Adapa baa44e91a2
*: Update imports statements from plinth to freedombox
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2026-08-28 07:35:18 -04:00

215 lines
5.9 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for first boot wizard.
"""
import operator
import os
import pathlib
import secrets
import string
import sys
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_noop
from freedombox import action_utils
from freedombox import app as app_module
from freedombox import cfg
from freedombox.signals import post_setup
first_boot_steps = [
{
'id': 'firstboot_welcome',
'url': 'first_boot:welcome',
'order': 0
},
]
_all_first_boot_steps = None
_is_completed = None
class FirstBootApp(app_module.App):
"""FreedomBox app for First Boot."""
app_id = 'first_boot'
_version = 1
def __init__(self) -> None:
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('First Boot'))
self.add(info)
def post_init(self):
"""Perform post initialization operations."""
post_setup.connect(_clear_first_boot_steps)
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
_firstboot_wizard_secret_create()
if not old_version:
self._show_next_steps_notification()
self.enable()
def _show_next_steps_notification(self):
"""After first setup, show notification for next steps."""
from freedombox.notification import Notification
title = gettext_noop('Setup complete! Next steps:')
message = gettext_noop(
'Initial setup has been completed. Perform the next steps to make '
'your {box_name} operational.')
data = {
'app_name': 'translate:' + gettext_noop('Next steps'),
'app_icon': 'fa-arrow-right',
'box_name': 'translate:' + cfg.box_name
}
actions = [{
'type': 'link',
'class': 'primary',
'text': gettext_noop('See next steps'),
'url': 'first_boot:complete'
}, {
'type': 'dismiss'
}]
Notification.update_or_create(id='first-boot-complete',
app_id='first_boot', severity='info',
title=title, message=message,
actions=actions, data=data,
group='admin', dismissed=False)
def _clear_first_boot_steps(sender, module_name, **kwargs):
"""Flush the cache of first boot steps so it is recreated."""
global _all_first_boot_steps
_all_first_boot_steps = None
def is_firstboot_url(path):
"""Return whether a path is a firstboot step URL.
:param path: path of url to be checked
:return: true if its a first boot URL false otherwise
"""
for step in _get_steps():
if path.startswith(reverse(step['url'])):
return True
return False
def _get_steps():
"""Return list of all firstboot steps."""
global _all_first_boot_steps
if _all_first_boot_steps is not None:
return _all_first_boot_steps
steps = []
for app in app_module.App.list():
module = sys.modules[app.__module__]
if getattr(module, 'first_boot_steps', None):
if not app.needs_setup():
steps.extend(module.first_boot_steps)
_all_first_boot_steps = sorted(steps, key=operator.itemgetter('order'))
return _all_first_boot_steps
def next_step():
"""Return the resolved next first boot step URL required to go to.
If there are no more step remaining, return 'complete' page.
"""
return next_step_or_none() or 'first_boot:complete'
def next_step_or_none():
"""Return the next first boot step required to run.
If there are no more step remaining, return None.
"""
from freedombox import kvstore
for step in _get_steps():
done = kvstore.get_default(step['id'], 0)
if not done:
return step.get('url')
def mark_step_done(id):
"""Marks the status of a first boot step as done.
:param id: id of the firstboot step
"""
from freedombox import kvstore
kvstore.set(id, 1)
if not next_step_or_none():
set_completed()
def is_completed():
"""Return whether first boot process is completed."""
from freedombox import kvstore
global _is_completed
if _is_completed is None:
_is_completed = kvstore.get_default('firstboot_completed', 0)
return bool(_is_completed)
def set_completed():
"""Set the first boot process as completed."""
from freedombox import kvstore
global _is_completed
_is_completed = True
kvstore.set('firstboot_completed', 1)
def get_secret_file_path():
"""Returns the path to the first boot wizard secret file."""
return os.path.join(cfg.data_dir, 'firstboot-wizard-secret')
def firstboot_wizard_secret_exists():
"""Return whether a firstboot wizard secret exists."""
secret_file = get_secret_file_path()
return os.path.exists(secret_file) and os.path.getsize(secret_file) > 0
def _generate_secret_key():
"""Generate a new random secret key for firstboot wizard."""
chars = string.ascii_letters + string.digits
return ''.join(secrets.choice(chars) for _ in range(16))
def _firstboot_wizard_secret_create():
"""Create a file with firstboot wizard secret if not in a disk image."""
if action_utils.is_disk_image():
return # On a disk image, first wizard secret is not asked.
path = pathlib.Path(get_secret_file_path())
if path.exists():
return # Secret already exists, don't change it.
secret = _generate_secret_key()
def opener(path, flags):
"""Create and open a file with restricted permissions."""
return os.open(path, flags, 0o400)
with open(path, mode='w', opener=opener) as file_handle:
file_handle.write(secret + '\n')