flshbd 0.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flshbd/__about__.py +4 -0
- flshbd/__init__.py +10 -0
- flshbd/__main__.py +9 -0
- flshbd/cli/__init__.py +66 -0
- flshbd/cli/access.py +153 -0
- flshbd/cli/dash.py +192 -0
- flshbd/cli/db.py +195 -0
- flshbd/cli/groups.py +225 -0
- flshbd/cli/process.py +332 -0
- flshbd/cli/site_groups.py +225 -0
- flshbd/cli/sites.py +325 -0
- flshbd/cli/users.py +317 -0
- flshbd/cli/utils.py +71 -0
- flshbd/core/.env +58 -0
- flshbd/core/__init__.py +8 -0
- flshbd/core/config.py +331 -0
- flshbd/core/env.py +211 -0
- flshbd/core/security.py +108 -0
- flshbd/data/__init__.py +8 -0
- flshbd/data/database.py +366 -0
- flshbd/data/models.py +1250 -0
- flshbd/web/__init__.py +8 -0
- flshbd/web/app.py +101 -0
- flshbd/web/auth.py +188 -0
- flshbd/web/auth_pam.py +62 -0
- flshbd/web/routes.py +208 -0
- flshbd/web/static/css/style.css +636 -0
- flshbd/web/static/images/FlashBoard-Logo.png +0 -0
- flshbd/web/templates/base.html +14 -0
- flshbd/web/templates/dash.html +20 -0
- flshbd/web/templates/login.html +56 -0
- flshbd/web/templates/partials/demo_site.html +33 -0
- flshbd/web/templates/partials/footer.html +3 -0
- flshbd/web/templates/partials/header.html +23 -0
- flshbd/web/templates/partials/help_site.html +46 -0
- flshbd/web/templates/partials/info_site.html +37 -0
- flshbd/web/templates/partials/main.html +25 -0
- flshbd/web/templates/partials/sidebar.html +23 -0
- flshbd/web/templates/partials/template_site.html +26 -0
- flshbd-0.0.9.dist-info/METADATA +106 -0
- flshbd-0.0.9.dist-info/RECORD +44 -0
- flshbd-0.0.9.dist-info/WHEEL +4 -0
- flshbd-0.0.9.dist-info/entry_points.txt +2 -0
- flshbd-0.0.9.dist-info/licenses/LICENSE.txt +10 -0
flshbd/__about__.py
ADDED
flshbd/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Unlicense
|
|
4
|
+
|
|
5
|
+
from flshbd.core.env import bootstrap
|
|
6
|
+
|
|
7
|
+
# Load the environment's .env file (if any) before any flshbd submodule is
|
|
8
|
+
# imported -- in particular before flshbd.core.config, whose FLSHBD_* values
|
|
9
|
+
# are read from the environment at import time. No-op under pytest.
|
|
10
|
+
bootstrap()
|
flshbd/__main__.py
ADDED
flshbd/cli/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Unlicense
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from flshbd.__about__ import __version__
|
|
7
|
+
from flshbd.cli.access import get_access_commands
|
|
8
|
+
from flshbd.cli.dash import get_dash_commands
|
|
9
|
+
from flshbd.cli.db import get_database_commands
|
|
10
|
+
from flshbd.cli.groups import get_group_commands
|
|
11
|
+
from flshbd.cli.site_groups import get_site_group_commands
|
|
12
|
+
from flshbd.cli.sites import get_site_commands
|
|
13
|
+
from flshbd.cli.users import get_user_commands
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(
|
|
17
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
18
|
+
invoke_without_command=True,
|
|
19
|
+
)
|
|
20
|
+
@click.option(
|
|
21
|
+
"--env",
|
|
22
|
+
"-e",
|
|
23
|
+
"env",
|
|
24
|
+
default="default",
|
|
25
|
+
type=click.Choice(["development", "production", "testing", "default"]),
|
|
26
|
+
help="Environment to use (development, production, testing, default)",
|
|
27
|
+
)
|
|
28
|
+
@click.version_option(version=__version__, prog_name="fld")
|
|
29
|
+
@click.pass_context
|
|
30
|
+
def fld(ctx, env: str):
|
|
31
|
+
"""FlashBoard CLI"""
|
|
32
|
+
# Set the current config for this process
|
|
33
|
+
from flshbd.core.config import set_current_config
|
|
34
|
+
|
|
35
|
+
set_current_config(env)
|
|
36
|
+
# Store environment in context for all subcommands
|
|
37
|
+
ctx.obj = {"env": env}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Add dash subcommands
|
|
41
|
+
dash = get_dash_commands()
|
|
42
|
+
fld.add_command(dash, "dash")
|
|
43
|
+
|
|
44
|
+
# Add database management commands
|
|
45
|
+
db = get_database_commands()
|
|
46
|
+
fld.add_command(db, "db")
|
|
47
|
+
|
|
48
|
+
# Add user management commands
|
|
49
|
+
user_cmd = get_user_commands()
|
|
50
|
+
fld.add_command(user_cmd, "user")
|
|
51
|
+
|
|
52
|
+
# Add group management commands
|
|
53
|
+
group_cmd = get_group_commands()
|
|
54
|
+
fld.add_command(group_cmd, "group")
|
|
55
|
+
|
|
56
|
+
# Add site management commands
|
|
57
|
+
site_cmd = get_site_commands()
|
|
58
|
+
fld.add_command(site_cmd, "site")
|
|
59
|
+
|
|
60
|
+
# Add site group management commands
|
|
61
|
+
site_group_cmd = get_site_group_commands()
|
|
62
|
+
fld.add_command(site_group_cmd, "site-group")
|
|
63
|
+
|
|
64
|
+
# Add access control commands
|
|
65
|
+
access_cmd = get_access_commands()
|
|
66
|
+
fld.add_command(access_cmd, "access")
|
flshbd/cli/access.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Unlicense
|
|
4
|
+
"""
|
|
5
|
+
CLI commands for access control management in FlashBoard
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from flshbd.cli.utils import format_table
|
|
13
|
+
from flshbd.data.database import get_database
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_db():
|
|
17
|
+
"""Get the configured database instance for the current environment."""
|
|
18
|
+
return get_database()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def access(ctx):
|
|
24
|
+
"""Access control management commands."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@access.command()
|
|
29
|
+
@click.argument("group_name")
|
|
30
|
+
@click.argument("site_group_name")
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def grant(ctx, group_name: str, site_group_name: str):
|
|
33
|
+
"""
|
|
34
|
+
Grant a user group access to a site group.
|
|
35
|
+
"""
|
|
36
|
+
db_instance = get_db()
|
|
37
|
+
|
|
38
|
+
with db_instance.get_connection() as conn:
|
|
39
|
+
cursor = conn.cursor()
|
|
40
|
+
|
|
41
|
+
# Get group IDs (try both group_id and name)
|
|
42
|
+
user_group = cursor.execute(
|
|
43
|
+
"SELECT id FROM user_groups WHERE group_id = ? OR name = ?", (group_name, group_name)
|
|
44
|
+
).fetchone()
|
|
45
|
+
|
|
46
|
+
if not user_group:
|
|
47
|
+
click.echo(f"User group '{group_name}' not found.")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
site_group = cursor.execute(
|
|
51
|
+
"SELECT id FROM site_groups WHERE group_id = ? OR name = ?", (site_group_name, site_group_name)
|
|
52
|
+
).fetchone()
|
|
53
|
+
|
|
54
|
+
if not site_group:
|
|
55
|
+
click.echo(f"Site group '{site_group_name}' not found.")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
# Check if access already exists
|
|
59
|
+
existing = cursor.execute(
|
|
60
|
+
"""SELECT * FROM group_access
|
|
61
|
+
WHERE group_id = ? AND site_group_id = ?""",
|
|
62
|
+
(user_group["id"], site_group["id"]),
|
|
63
|
+
).fetchone()
|
|
64
|
+
|
|
65
|
+
if existing:
|
|
66
|
+
click.echo(f"Group '{group_name}' already has access to site group '{site_group_name}'.")
|
|
67
|
+
sys.exit(0)
|
|
68
|
+
|
|
69
|
+
# Grant access
|
|
70
|
+
cursor.execute(
|
|
71
|
+
"""INSERT INTO group_access (group_id, site_group_id)
|
|
72
|
+
VALUES (?, ?)""",
|
|
73
|
+
(user_group["id"], site_group["id"]),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
click.echo(f"Access granted: '{group_name}' can now access '{site_group_name}'.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@access.command()
|
|
80
|
+
@click.argument("group_name")
|
|
81
|
+
@click.argument("site_group_name")
|
|
82
|
+
@click.pass_context
|
|
83
|
+
def revoke(ctx, group_name: str, site_group_name: str):
|
|
84
|
+
"""
|
|
85
|
+
Revoke a user group's access to a site group.
|
|
86
|
+
"""
|
|
87
|
+
db_instance = get_db()
|
|
88
|
+
|
|
89
|
+
with db_instance.get_connection() as conn:
|
|
90
|
+
cursor = conn.cursor()
|
|
91
|
+
|
|
92
|
+
# Get group IDs (try both group_id and name)
|
|
93
|
+
user_group = cursor.execute(
|
|
94
|
+
"SELECT id FROM user_groups WHERE group_id = ? OR name = ?", (group_name, group_name)
|
|
95
|
+
).fetchone()
|
|
96
|
+
|
|
97
|
+
if not user_group:
|
|
98
|
+
click.echo(f"User group '{group_name}' not found.")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
site_group = cursor.execute(
|
|
102
|
+
"SELECT id FROM site_groups WHERE group_id = ? OR name = ?", (site_group_name, site_group_name)
|
|
103
|
+
).fetchone()
|
|
104
|
+
|
|
105
|
+
if not site_group:
|
|
106
|
+
click.echo(f"Site group '{site_group_name}' not found.")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
# Revoke access
|
|
110
|
+
cursor.execute(
|
|
111
|
+
"""DELETE FROM group_access
|
|
112
|
+
WHERE group_id = ? AND site_group_id = ?""",
|
|
113
|
+
(user_group["id"], site_group["id"]),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
click.echo(f"Access revoked: '{group_name}' can no longer access '{site_group_name}'.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@access.command()
|
|
120
|
+
@click.pass_context
|
|
121
|
+
def list(ctx):
|
|
122
|
+
"""
|
|
123
|
+
List all access control rules.
|
|
124
|
+
"""
|
|
125
|
+
db_instance = get_db()
|
|
126
|
+
|
|
127
|
+
with db_instance.get_connection() as conn:
|
|
128
|
+
cursor = conn.cursor()
|
|
129
|
+
|
|
130
|
+
query = """
|
|
131
|
+
SELECT
|
|
132
|
+
ug.name as user_group_name,
|
|
133
|
+
sg.name as site_group_name
|
|
134
|
+
FROM group_access ga
|
|
135
|
+
JOIN user_groups ug ON ga.group_id = ug.id
|
|
136
|
+
JOIN site_groups sg ON ga.site_group_id = sg.id
|
|
137
|
+
ORDER BY ug.name, sg.name
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
cursor.execute(query)
|
|
141
|
+
rows = [dict(row) for row in cursor.fetchall()]
|
|
142
|
+
|
|
143
|
+
if not rows:
|
|
144
|
+
click.echo("No access control rules found.")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
headers = ["user_group_name", "site_group_name"]
|
|
148
|
+
click.echo(format_table(rows, headers))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_access_commands():
|
|
152
|
+
"""Get the access command group for registration."""
|
|
153
|
+
return access
|
flshbd/cli/dash.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Unlicense
|
|
4
|
+
"""
|
|
5
|
+
CLI commands for the FlashBoard
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from flshbd.cli.process import ProcessManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def dash():
|
|
17
|
+
"""Dashboard management commands"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dash.command()
|
|
22
|
+
@click.option("--port", "-p", default=5000, type=int, help="Port to run the server on")
|
|
23
|
+
@click.option("--host", "-H", default="localhost", help="Host to bind the server to")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--server",
|
|
26
|
+
"-s",
|
|
27
|
+
"server_type",
|
|
28
|
+
default="flask",
|
|
29
|
+
type=click.Choice(["flask", "gunicorn"]),
|
|
30
|
+
help="Server type to use (flask or gunicorn)",
|
|
31
|
+
)
|
|
32
|
+
@click.pass_context
|
|
33
|
+
def start(ctx, port: int, host: str, server_type: str):
|
|
34
|
+
"""
|
|
35
|
+
Start the FlashBoard server.
|
|
36
|
+
|
|
37
|
+
This command starts the dashboard server with the specified configuration.
|
|
38
|
+
In development mode with Flask, debug is enabled.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
fld dash start # Start Flask in development mode
|
|
42
|
+
fld --env production dash start # Start Flask in production mode
|
|
43
|
+
fld --env production dash start --server gunicorn # Start with Gunicorn
|
|
44
|
+
fld dash start --port 8080 # Start on port 8080
|
|
45
|
+
"""
|
|
46
|
+
# Get current config
|
|
47
|
+
from flshbd.core.config import get_current_config
|
|
48
|
+
|
|
49
|
+
config = get_current_config()
|
|
50
|
+
env = config.ENV
|
|
51
|
+
|
|
52
|
+
# ProcessManager will use the current config set by CLI
|
|
53
|
+
pm = ProcessManager()
|
|
54
|
+
|
|
55
|
+
# Check if server is already running
|
|
56
|
+
if pm.is_running():
|
|
57
|
+
click.echo("Dashboard server is already running.")
|
|
58
|
+
status_info = pm.status()
|
|
59
|
+
if status_info.get("running"):
|
|
60
|
+
click.echo(f"PID: {status_info['pid']}")
|
|
61
|
+
click.echo(f"URL: http://{status_info['host']}:{status_info['port']}")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
click.echo("Starting FlashBoard server...")
|
|
65
|
+
click.echo(f"Environment: {env}")
|
|
66
|
+
click.echo(f"URL: http://{host}:{port}")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
if server_type == "gunicorn":
|
|
70
|
+
if env != "production":
|
|
71
|
+
click.echo("Warning: Gunicorn is recommended for production only", err=True)
|
|
72
|
+
pid, log_file = pm.start_gunicorn(env, port, host)
|
|
73
|
+
click.echo("Server type: Gunicorn")
|
|
74
|
+
else:
|
|
75
|
+
debug = env == "development"
|
|
76
|
+
pid, log_file = pm.start_flask(env, port, host, debug)
|
|
77
|
+
click.echo("Server type: Flask")
|
|
78
|
+
if debug:
|
|
79
|
+
click.echo("Debug mode: enabled (debug pin available in log file)")
|
|
80
|
+
|
|
81
|
+
click.echo(f"Dashboard started with PID: {pid}")
|
|
82
|
+
click.echo(f"Log file: {log_file}")
|
|
83
|
+
click.echo("Server is running in the background.")
|
|
84
|
+
click.echo("Use 'fld dash stop' to stop the server.")
|
|
85
|
+
click.echo("Use 'fld dash status' to check server status.")
|
|
86
|
+
|
|
87
|
+
except FileNotFoundError as e:
|
|
88
|
+
if "gunicorn" in str(e):
|
|
89
|
+
click.echo("Error: Gunicorn is not installed.", err=True)
|
|
90
|
+
click.echo("Install it with: pip install flshbd[gunicorn]", err=True)
|
|
91
|
+
else:
|
|
92
|
+
click.echo(f"Command not found: {e}", err=True)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
except RuntimeError as e:
|
|
95
|
+
click.echo(f"Error: {e}", err=True)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
click.echo(f"Error starting server: {e}", err=True)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dash.command()
|
|
103
|
+
@click.pass_context
|
|
104
|
+
def stop(ctx):
|
|
105
|
+
"""
|
|
106
|
+
Stop the running FlashBoard server.
|
|
107
|
+
|
|
108
|
+
This command stops the server that was started with 'fld dash start'.
|
|
109
|
+
It sends a SIGTERM signal for graceful shutdown.
|
|
110
|
+
"""
|
|
111
|
+
# ProcessManager will use the current config set by CLI
|
|
112
|
+
pm = ProcessManager()
|
|
113
|
+
|
|
114
|
+
if not pm.is_running():
|
|
115
|
+
click.echo("No dashboard server is running.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
status_info = pm.status()
|
|
119
|
+
click.echo(f"Stopping dashboard server (PID: {status_info['pid']})...")
|
|
120
|
+
|
|
121
|
+
if pm.stop():
|
|
122
|
+
click.echo("Dashboard server stopped.")
|
|
123
|
+
else:
|
|
124
|
+
click.echo("Failed to stop dashboard server.", err=True)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dash.command()
|
|
128
|
+
@click.pass_context
|
|
129
|
+
def status(ctx):
|
|
130
|
+
"""
|
|
131
|
+
Show the status of the FlashBoard server.
|
|
132
|
+
|
|
133
|
+
This command displays information about the currently running server,
|
|
134
|
+
including PID, environment, port, and log file location.
|
|
135
|
+
"""
|
|
136
|
+
# ProcessManager will use the current config set by CLI
|
|
137
|
+
pm = ProcessManager()
|
|
138
|
+
status_info = pm.status()
|
|
139
|
+
|
|
140
|
+
if status_info.get("running"):
|
|
141
|
+
click.echo("Dashboard server is RUNNING")
|
|
142
|
+
click.echo(f" PID: {status_info['pid']}")
|
|
143
|
+
click.echo(f" Environment: {status_info['env']}")
|
|
144
|
+
click.echo(f" URL: http://{status_info['host']}:{status_info['port']}")
|
|
145
|
+
click.echo(f" Started: {status_info['start_time']}")
|
|
146
|
+
click.echo(f" Log file: {status_info['log_file']}")
|
|
147
|
+
if status_info.get("debug_pin"):
|
|
148
|
+
click.echo(f" Debug pin: {status_info['debug_pin']}")
|
|
149
|
+
if status_info.get("message"):
|
|
150
|
+
click.echo(f" Note: {status_info['message']}")
|
|
151
|
+
else:
|
|
152
|
+
click.echo("Dashboard server is NOT RUNNING")
|
|
153
|
+
if "message" in status_info:
|
|
154
|
+
click.echo(f" {status_info['message']}")
|
|
155
|
+
if "pid" in status_info:
|
|
156
|
+
click.echo(f" Last PID: {status_info['pid']}")
|
|
157
|
+
if "env" in status_info:
|
|
158
|
+
click.echo(f" Last environment: {status_info['env']}")
|
|
159
|
+
if "port" in status_info:
|
|
160
|
+
click.echo(f" Last port: {status_info['port']}")
|
|
161
|
+
if "log_file" in status_info:
|
|
162
|
+
click.echo(f" Last log file: {status_info['log_file']}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dash.command()
|
|
166
|
+
def generate_secret_key():
|
|
167
|
+
"""
|
|
168
|
+
Generate a new secret key for configuration.
|
|
169
|
+
|
|
170
|
+
This generates a cryptographically secure secret key that can be used
|
|
171
|
+
as the FLSHBD_SECRET_KEY environment variable. When FLSHBD_SECRET_KEY is set via
|
|
172
|
+
environment variable, ephemeral key generation is disabled and all
|
|
173
|
+
workers will use this persistent key.
|
|
174
|
+
|
|
175
|
+
To use the generated key:
|
|
176
|
+
export FLSHBD_SECRET_KEY=<generated-key>
|
|
177
|
+
|
|
178
|
+
Or add it to your environment configuration file.
|
|
179
|
+
"""
|
|
180
|
+
import secrets
|
|
181
|
+
|
|
182
|
+
key = secrets.token_hex(32)
|
|
183
|
+
click.echo(f"FLSHBD_SECRET_KEY={key}")
|
|
184
|
+
click.echo("\nTo use this key, set it as an environment variable:")
|
|
185
|
+
click.echo(f" export FLSHBD_SECRET_KEY={key}")
|
|
186
|
+
click.echo("\nNote: Setting FLSHBD_SECRET_KEY disables ephemeral key generation.")
|
|
187
|
+
click.echo(" All server restarts will use this key until it is changed.")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_dash_commands():
|
|
191
|
+
"""Get the dash command group for registration"""
|
|
192
|
+
return dash
|
flshbd/cli/db.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Unlicense
|
|
4
|
+
"""
|
|
5
|
+
CLI commands for database management in FlashBoard
|
|
6
|
+
|
|
7
|
+
This module provides CLI commands for database initialization, backup, and restore.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from flshbd.core.config import get_current_config
|
|
16
|
+
from flshbd.data.database import Database, get_database
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_db() -> Database:
|
|
20
|
+
"""Get the configured database instance for the current environment."""
|
|
21
|
+
return get_database()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def db(ctx):
|
|
27
|
+
"""Database management commands."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@db.command()
|
|
32
|
+
@click.option("--force", is_flag=True, help="Overwrite existing database if it exists")
|
|
33
|
+
@click.pass_context
|
|
34
|
+
def init(ctx, force: bool):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the database with schema.
|
|
37
|
+
|
|
38
|
+
Creates all required tables for users, groups, sites, and access control.
|
|
39
|
+
If the database already exists and --force is not specified, the command
|
|
40
|
+
will fail.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
config = get_current_config()
|
|
44
|
+
db_instance = Database(config.DB_PATH)
|
|
45
|
+
|
|
46
|
+
if os.path.exists(db_instance.db_path) and not force:
|
|
47
|
+
click.echo(f"Database already exists at: {db_instance.db_path}")
|
|
48
|
+
click.echo("Use --force to overwrite the existing database.")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
click.echo(f"Initializing database at: {db_instance.db_path}")
|
|
52
|
+
db_instance.initialize(force=force)
|
|
53
|
+
click.echo("Database initialized successfully.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@db.command()
|
|
57
|
+
@click.pass_context
|
|
58
|
+
def migrate(ctx):
|
|
59
|
+
"""
|
|
60
|
+
Seed the database with the static demo data.
|
|
61
|
+
|
|
62
|
+
Populates an empty database with the sample data from the static models
|
|
63
|
+
(users, groups, sites, site groups, access). Does nothing if the database
|
|
64
|
+
already contains users. This is the explicit seeding command for a new
|
|
65
|
+
database; the schema itself is created automatically at server start.
|
|
66
|
+
"""
|
|
67
|
+
from flshbd.data.models import seed_static_data
|
|
68
|
+
|
|
69
|
+
db_instance = get_db()
|
|
70
|
+
|
|
71
|
+
# Check if database exists and has tables
|
|
72
|
+
if not db_instance.table_exists("users"):
|
|
73
|
+
click.echo("Database not initialized. Running 'db init' first...")
|
|
74
|
+
db_instance.initialize()
|
|
75
|
+
|
|
76
|
+
seed_static_data()
|
|
77
|
+
|
|
78
|
+
click.echo("Data migration completed successfully.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@db.command()
|
|
82
|
+
@click.option("--path", "-p", default=None, help="Backup file path. Defaults to timestamped file in same directory.")
|
|
83
|
+
@click.pass_context
|
|
84
|
+
def backup(ctx, path: str | None):
|
|
85
|
+
"""
|
|
86
|
+
Create a backup of the current database.
|
|
87
|
+
|
|
88
|
+
Creates a copy of the database file. If no path is specified,
|
|
89
|
+
creates a timestamped backup in the same directory as the database.
|
|
90
|
+
"""
|
|
91
|
+
db_instance = get_db()
|
|
92
|
+
|
|
93
|
+
if not os.path.exists(db_instance.db_path):
|
|
94
|
+
click.echo(f"Database file not found: {db_instance.db_path}")
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
backup_path = db_instance.backup(path)
|
|
99
|
+
click.echo(f"Backup created at: {backup_path}")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
click.echo(f"Failed to create backup: {e}")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@db.command()
|
|
106
|
+
@click.argument("backup_path")
|
|
107
|
+
@click.pass_context
|
|
108
|
+
def restore(ctx, backup_path: str):
|
|
109
|
+
"""
|
|
110
|
+
Restore database from a backup file.
|
|
111
|
+
|
|
112
|
+
Replaces the current database with the specified backup file.
|
|
113
|
+
"""
|
|
114
|
+
db_instance = get_db()
|
|
115
|
+
|
|
116
|
+
backup_path = os.path.expanduser(backup_path)
|
|
117
|
+
if not os.path.exists(backup_path):
|
|
118
|
+
click.echo(f"Backup file not found: {backup_path}")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
click.echo(f"Restoring database from: {backup_path}")
|
|
123
|
+
db_instance.restore(backup_path)
|
|
124
|
+
click.echo("Database restored successfully.")
|
|
125
|
+
except Exception as e:
|
|
126
|
+
click.echo(f"Failed to restore database: {e}")
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@db.group()
|
|
131
|
+
@click.pass_context
|
|
132
|
+
def config(ctx):
|
|
133
|
+
"""Manage database configuration key/value pairs."""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@config.command("get")
|
|
138
|
+
@click.argument("key")
|
|
139
|
+
@click.pass_context
|
|
140
|
+
def config_get(ctx, key: str):
|
|
141
|
+
"""
|
|
142
|
+
Get a configuration value by key.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
key: The configuration key to retrieve
|
|
146
|
+
"""
|
|
147
|
+
from flshbd.data.models import get_config_value
|
|
148
|
+
|
|
149
|
+
value = get_config_value(key)
|
|
150
|
+
if value is not None:
|
|
151
|
+
click.echo(f"{key} = {value}")
|
|
152
|
+
else:
|
|
153
|
+
click.echo(f"Configuration key '{key}' not found.")
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@config.command("set")
|
|
158
|
+
@click.argument("key")
|
|
159
|
+
@click.argument("value")
|
|
160
|
+
@click.pass_context
|
|
161
|
+
def config_set(ctx, key: str, value: str):
|
|
162
|
+
"""
|
|
163
|
+
Set a configuration key/value pair.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
key: The configuration key
|
|
167
|
+
value: The configuration value
|
|
168
|
+
"""
|
|
169
|
+
from flshbd.data.models import set_config_value
|
|
170
|
+
|
|
171
|
+
if set_config_value(key, value):
|
|
172
|
+
click.echo(f"Configuration set: {key} = {value}")
|
|
173
|
+
else:
|
|
174
|
+
click.echo(f"Failed to set configuration: {key} = {value}")
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@config.command("list")
|
|
179
|
+
@click.pass_context
|
|
180
|
+
def config_list(ctx):
|
|
181
|
+
"""List all configuration entries."""
|
|
182
|
+
from flshbd.data.models import get_all_config_entries
|
|
183
|
+
|
|
184
|
+
entries = get_all_config_entries()
|
|
185
|
+
if entries:
|
|
186
|
+
click.echo("Configuration entries:")
|
|
187
|
+
for entry in entries:
|
|
188
|
+
click.echo(f" {entry.key} = {entry.value}")
|
|
189
|
+
else:
|
|
190
|
+
click.echo("No configuration entries found.")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_database_commands():
|
|
194
|
+
"""Get the database command group for registration."""
|
|
195
|
+
return db
|