Coverage for seedboxsync/cli/commands/cmd_user.py: 90%
87 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 17:10 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 17:10 +0000
1#
2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
7"""All commands related to the users operations & management."""
9import click
10from peewee import fn
11from werkzeug.security import generate_password_hash
12from seedboxsync.cli import Context, group, pass_context
13from seedboxsync.core.database.models import User
16@group("user", help="User operations & management for SeedboxSync frontend.") # type: ignore[untyped-decorator]
17@pass_context
18def cli(ctx: Context) -> None:
19 """Empty function for Click sub commands."""
22@cli.command("list", help="List users.") # type: ignore[untyped-decorator]
23@click.option("-n", "--number", type=int, default=10, help="Number of torrents to display.")
24@click.option("-s", "--search", help="Term to search.")
25@pass_context
26def list_user(ctx: Context, number: int, search: str) -> None:
27 """
28 List registered users from the database.
30 Queries user accounts with optional text filtering on usernames and renders
31 a formatted table containing user metadata.
33 Args:
34 ctx (Context): The CLI application context.
35 number (int): Maximum number of users to display. Defaults to 10.
36 search (str): Optional search term to filter users by username.
37 """
38 # Build "where" expression
39 conditions = []
40 if search:
41 conditions.append(User.username.contains(search))
43 # DB query
44 query = (
45 User.select(
46 User.id,
47 User.username,
48 User.email,
49 User.origin,
50 User.is_admin,
51 fn.short_datetime(User.created),
52 fn.short_datetime(User.last_login),
53 )
54 .limit(number)
55 .order_by(User.id.desc())
56 )
58 # if "where" expression
59 if conditions:
60 query = query.where(*conditions)
61 data = query.dicts()
63 click.echo(
64 ctx.render(
65 reversed(data),
66 headers={
67 "id": "Id",
68 "username": "Username",
69 "email": "Email",
70 "origin": "Origin",
71 "is_admin": "Is Admin",
72 "created": "Created",
73 "last_login": "Last Login",
74 },
75 )
76 )
79@cli.command("delete", help="Delete a user.") # type: ignore[untyped-decorator]
80@click.option("--id", type=int, required=True, help="ID of the user to delete.")
81@click.option("-y", "--yes", is_flag=True, help="Confirm deletion without prompting.")
82@pass_context
83def delete(ctx: Context, id: int, yes: bool) -> None: # noqa: A002
84 """
85 Delete a user by their unique database identifier.
87 Args:
88 ctx (Context): The CLI application context.
89 id (int): Database identifier of the user to remove.
90 yes (bool): Skip confirmation prompt if set to True.
91 """
92 try:
93 user = User.get_by_id(id)
94 except User.DoesNotExist: # pyright: ignore [reportAttributeAccessIssue]
95 click.secho(f"Error: User with ID {id} does not exist.", fg="red", err=True)
96 return
98 if not yes and not click.confirm(f"Are you sure you want to delete user '{user.username}' (ID: {user.id})?"):
99 click.echo("Operation canceled.")
100 return
102 try:
103 username = user.username
104 user.delete_instance()
105 click.secho(f"User '{username}' (ID: {id}) deleted successfully.", fg="green")
106 except Exception as e:
107 click.secho(f"Error: Failed to delete user {id}: {e}", fg="red", err=True)
110@cli.command("add", help="Add a new user.") # type: ignore[untyped-decorator]
111@click.option("-u", "--username", prompt="Username", help="Username for the new account.")
112@click.option("-e", "--email", prompt="Email address", help="Unique email address.")
113@click.option("-p", "--password", prompt=True, hide_input=True, confirmation_prompt=True, help="Password for the user account.")
114@click.option("--is-admin/--no-is-admin", "is_admin", prompt="Grant admin privileges?", is_flag=True, help="Grant admin privileges to the user.")
115@pass_context
116def add(ctx: Context, username: str, email: str, password: str, is_admin: bool) -> None:
117 """
118 Create a new user account in the database.
120 Accepts account details via command-line options or interactive prompts,
121 hashes the password, and stores the user record.
123 Args:
124 ctx (Context): The CLI application context.
125 username (str): Unique username for the account.
126 email (str): Unique email address.
127 password (str): Plain-text password to hash and store.
128 is_admin (bool): Whether the user should have admin privileges.
129 """
130 # Verify if user already exists before attempting insertion
131 if User.get_or_none((User.username == username) | (User.email == email)):
132 click.secho(
133 f"Error: A user with username '{username}' or email '{email}' already exists.",
134 fg="red",
135 err=True,
136 )
137 return
139 try:
140 user = User.create(username=username, email=email, password=generate_password_hash(password), is_admin=is_admin)
141 click.secho(f"User '{user.username}' (ID: {user.id}) created successfully.", fg="green")
142 except Exception as e:
143 click.secho(f"Error: Failed to create user: {e}", fg="red", err=True)
146@cli.command("edit", help="Edit an existing user.") # type: ignore[untyped-decorator]
147@click.option("--id", type=int, required=True, help="ID of the user to edit.")
148@click.option("-u", "--username", help="New username for the account.")
149@click.option("-e", "--email", help="New email address.")
150@click.option("-p", "--password", help="New password for the account.")
151@click.option("--is-admin/--no-is-admin", "is_admin", default=None, help="Grant or revoke admin privileges.")
152@pass_context
153def edit(ctx: Context, id: int, username: str | None, email: str | None, password: str | None, is_admin: bool | None) -> None: # noqa: A002
154 """
155 Update details for an existing user account.
157 Fetches the user by ID and updates only the provided fields. Password
158 updates are automatically hashed before saving.
160 Args:
161 ctx (Context): The CLI application context.
162 id (int): Database identifier of the user to edit.
163 username (str | None): New username to set.
164 email (str | None): New email address to set.
165 password (str | None): New plain-text password to hash and update.
166 is_admin (bool | None): Whether the user should have admin privileges.
167 """
168 try:
169 user = User.get_by_id(id)
170 except User.DoesNotExist: # pyright: ignore [reportAttributeAccessIssue]
171 click.secho(f"Error: User with ID {id} does not exist.", fg="red", err=True)
172 return
174 # Check unique constraint collisions if username or email is being updated
175 if username and username != user.username:
176 if User.get_or_none(User.username == username):
177 click.secho(f"Error: Username '{username}' is already taken.", fg="red", err=True)
178 return
179 user.username = username
181 if email and email != user.email:
182 if User.get_or_none(User.email == email):
183 click.secho(f"Error: Email '{email}' is already taken.", fg="red", err=True)
184 return
185 user.email = email
187 if password:
188 user.password = generate_password_hash(password)
190 if is_admin is not None:
191 user.is_admin = is_admin
193 try:
194 user.save()
195 click.secho(f"User '{user.username}' (ID: {user.id}) updated successfully.", fg="green")
196 except Exception as e:
197 click.secho(f"Error: Failed to update user {id}: {e}", fg="red", err=True)