Coverage for seedboxsync/cli/commands/cmd_user.py: 90%
83 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +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 fn.short_datetime(User.created),
51 fn.short_datetime(User.last_login),
52 )
53 .limit(number)
54 .order_by(User.id.desc())
55 )
57 # if "where" expression
58 if conditions:
59 query = query.where(*conditions)
60 data = query.dicts()
62 click.echo(
63 ctx.render(
64 reversed(data),
65 headers={"id": "Id", "username": "Username", "email": "Email", "origin": "Origin", "created": "Created", "last_login": "Last Login"},
66 )
67 )
70@cli.command("delete", help="Delete a user.") # type: ignore[untyped-decorator]
71@click.option("--id", type=int, required=True, help="ID of the user to delete.")
72@click.option("-y", "--yes", is_flag=True, help="Confirm deletion without prompting.")
73@pass_context
74def delete(ctx: Context, id: int, yes: bool) -> None: # noqa: A002
75 """
76 Delete a user by their unique database identifier.
78 Args:
79 ctx (Context): The CLI application context.
80 id (int): Database identifier of the user to remove.
81 yes (bool): Skip confirmation prompt if set to True.
82 """
83 try:
84 user = User.get_by_id(id)
85 except User.DoesNotExist: # pyright: ignore [reportAttributeAccessIssue]
86 click.secho(f"Error: User with ID {id} does not exist.", fg="red", err=True)
87 return
89 if not yes and not click.confirm(f"Are you sure you want to delete user '{user.username}' (ID: {user.id})?"):
90 click.echo("Operation canceled.")
91 return
93 try:
94 username = user.username
95 user.delete_instance()
96 click.secho(f"User '{username}' (ID: {id}) deleted successfully.", fg="green")
97 except Exception as e:
98 click.secho(f"Error: Failed to delete user {id}: {e}", fg="red", err=True)
101@cli.command("add", help="Add a new user.") # type: ignore[untyped-decorator]
102@click.option("-u", "--username", prompt="Username", help="Username for the new account.")
103@click.option("-e", "--email", prompt="Email address", help="Unique email address.")
104@click.option("-p", "--password", prompt=True, hide_input=True, confirmation_prompt=True, help="Password for the user account.")
105@pass_context
106def add(ctx: Context, username: str, email: str, password: str) -> None:
107 """
108 Create a new user account in the database.
110 Accepts account details via command-line options or interactive prompts,
111 hashes the password, and stores the user record.
113 Args:
114 ctx (Context): The CLI application context.
115 username (str): Unique username for the account.
116 email (str): Unique email address.
117 password (str): Plain-text password to hash and store.
118 """
119 # Verify if user already exists before attempting insertion
120 if User.get_or_none((User.username == username) | (User.email == email)):
121 click.secho(
122 f"Error: A user with username '{username}' or email '{email}' already exists.",
123 fg="red",
124 err=True,
125 )
126 return
128 try:
129 user = User.create(
130 username=username,
131 email=email,
132 password=generate_password_hash(password),
133 )
134 click.secho(f"User '{user.username}' (ID: {user.id}) created successfully.", fg="green")
135 except Exception as e:
136 click.secho(f"Error: Failed to create user: {e}", fg="red", err=True)
139@cli.command("edit", help="Edit an existing user.") # type: ignore[untyped-decorator]
140@click.option("--id", type=int, required=True, help="ID of the user to edit.")
141@click.option("-u", "--username", help="New username for the account.")
142@click.option("-e", "--email", help="New email address.")
143@click.option("-p", "--password", help="New password for the account.")
144@pass_context
145def edit(ctx: Context, id: int, username: str | None, email: str | None, password: str | None) -> None: # noqa: A002
146 """
147 Update details for an existing user account.
149 Fetches the user by ID and updates only the provided fields. Password
150 updates are automatically hashed before saving.
152 Args:
153 ctx (Context): The CLI application context.
154 id (int): Database identifier of the user to edit.
155 username (str | None): New username to set.
156 email (str | None): New email address to set.
157 password (str | None): New plain-text password to hash and update.
158 """
159 try:
160 user = User.get_by_id(id)
161 except User.DoesNotExist: # pyright: ignore [reportAttributeAccessIssue]
162 click.secho(f"Error: User with ID {id} does not exist.", fg="red", err=True)
163 return
165 # Check unique constraint collisions if username or email is being updated
166 if username and username != user.username:
167 if User.get_or_none(User.username == username):
168 click.secho(f"Error: Username '{username}' is already taken.", fg="red", err=True)
169 return
170 user.username = username
172 if email and email != user.email:
173 if User.get_or_none(User.email == email):
174 click.secho(f"Error: Email '{email}' is already taken.", fg="red", err=True)
175 return
176 user.email = email
178 if password:
179 user.password = generate_password_hash(password)
181 try:
182 user.save()
183 click.secho(f"User '{user.username}' (ID: {user.id}) updated successfully.", fg="green")
184 except Exception as e:
185 click.secho(f"Error: Failed to update user {id}: {e}", fg="red", err=True)