Skip to main content

wekan_cli/commands/
users.rs

1mod boards;
2mod cards;
3mod create;
4mod current;
5mod delete;
6mod disable_login;
7mod enable_login;
8mod get;
9mod list;
10mod take_ownership;
11mod token;
12mod update;
13
14#[cfg(test)]
15mod tests;
16
17use clap::{Args, Subcommand};
18
19use crate::{
20    client::{UserRecord, WekanClientFactory},
21    command_result::{
22        CommandSuccess, UserBoardMembership, UserDetail, UserDialogOptionsDetail, UserEmail,
23        UserOrganization, UserProfileDetail, UserProfileNotificationDetail,
24        UserProfileStarredPageDetail, UserSessionData, UserTeam, UserWorkspaceNodeDetail,
25    },
26    credentials::{CredentialStore, SecretInputProvider},
27    error::AppError,
28    input::ConfirmationProvider,
29};
30
31use super::client_error::map_client_error;
32
33#[derive(Debug, Args)]
34pub struct UserArgs {
35    #[command(subcommand)]
36    pub command: UserCommand,
37}
38
39#[derive(Debug, Subcommand)]
40pub enum UserCommand {
41    /// Create or revoke user API tokens (site admin only).
42    Token(token::TokenArgs),
43    /// Show the currently authenticated Wekan user.
44    Current(current::CurrentArgs),
45
46    /// List cards related to the currently authenticated user.
47    Cards(cards::CardsArgs),
48
49    /// List Wekan users (site admin only).
50    List(list::ListArgs),
51
52    /// Show a Wekan user by ID or username (site admin only).
53    Get(get::GetArgs),
54
55    /// Submit Wekan's no-action update and return the unchanged user (site admin only).
56    Update(update::UpdateArgs),
57
58    /// Create a Wekan user (site admin only).
59    Create(create::CreateArgs),
60
61    /// List active boards visible for a user ID.
62    Boards(boards::BoardsArgs),
63
64    /// Run Wekan's board ownership action for a user.
65    TakeOwnership(take_ownership::TakeOwnershipArgs),
66
67    /// Disable a user's login and clear their sessions.
68    DisableLogin(disable_login::DisableLoginArgs),
69
70    /// Re-enable a user's login.
71    EnableLogin(enable_login::EnableLoginArgs),
72
73    /// Delete a Wekan user.
74    Delete(delete::DeleteArgs),
75}
76
77pub(crate) async fn dispatch(
78    command: UserCommand,
79    client_factory: &WekanClientFactory,
80    credential_store: &dyn CredentialStore,
81    secret_input: &dyn SecretInputProvider,
82    confirmation: &dyn ConfirmationProvider,
83) -> Result<CommandSuccess, AppError> {
84    match command {
85        UserCommand::Token(args) => {
86            token::dispatch(
87                args.command,
88                client_factory,
89                credential_store,
90                secret_input,
91                confirmation,
92            )
93            .await
94        }
95        UserCommand::Current(args) => {
96            current::execute(args, client_factory, credential_store).await
97        }
98        UserCommand::Cards(args) => cards::execute(args, client_factory, credential_store).await,
99        UserCommand::List(args) => list::execute(args, client_factory, credential_store).await,
100        UserCommand::Get(args) => get::execute(args, client_factory, credential_store).await,
101        UserCommand::Update(args) => update::execute(args, client_factory, credential_store).await,
102        UserCommand::Create(args) => {
103            create::execute(args, client_factory, credential_store, secret_input).await
104        }
105        UserCommand::Boards(args) => boards::execute(args, client_factory, credential_store).await,
106        UserCommand::TakeOwnership(args) => {
107            take_ownership::execute(args, client_factory, credential_store, confirmation).await
108        }
109        UserCommand::DisableLogin(args) => {
110            disable_login::execute(args, client_factory, credential_store, confirmation).await
111        }
112        UserCommand::EnableLogin(args) => {
113            enable_login::execute(args, client_factory, credential_store).await
114        }
115        UserCommand::Delete(args) => {
116            delete::execute(args, client_factory, credential_store, confirmation).await
117        }
118    }
119}
120
121fn user_detail(user: UserRecord) -> UserDetail {
122    UserDetail {
123        passkeys: user.passkeys,
124        sqrl: user.sqrl,
125        web_push_subscriptions: user.web_push_subscriptions,
126        ntfy: user.ntfy,
127        notification_delivery_pending: user.notification_delivery_pending,
128
129        notification_delivery_revision: user.notification_delivery_revision,
130        feature_preview: user.feature_preview,
131        inactivity_clock_start: user.inactivity_clock_start,
132        login_disabled_reason: user.login_disabled_reason,
133        anonymized: user.anonymized,
134        anonymized_at: user.anonymized_at,
135
136        user_id: user.user_id,
137        username: user.username,
138        full_name: user.profile.fullname.clone(),
139        profile: Box::new(user_profile(*user.profile)),
140        created_through_api: user.created_through_api,
141        heartbeat: user.heartbeat,
142        session_data: user.session_data.map(|data| UserSessionData {
143            total_hits: data.total_hits,
144        }),
145        import_usernames: user.import_usernames,
146        emails: user
147            .emails
148            .into_iter()
149            .map(|email| UserEmail {
150                address: email.address,
151                verified: email.verified,
152            })
153            .collect(),
154        is_admin: user.is_admin,
155        login_disabled: user.login_disabled,
156        authentication_method: user.authentication_method,
157        created_at: user.created_at,
158        modified_at: user.modified_at,
159        last_connection_date: user.last_connection_date,
160        organizations: user
161            .orgs
162            .into_iter()
163            .map(|organization| UserOrganization {
164                organization_id: organization.org_id,
165                display_name: organization.org_display_name,
166                is_admin: organization.is_admin,
167            })
168            .collect(),
169        teams: user
170            .teams
171            .into_iter()
172            .map(|team| UserTeam {
173                team_id: team.team_id,
174                display_name: team.team_display_name,
175            })
176            .collect(),
177        boards: user
178            .boards
179            .into_iter()
180            .map(|board| UserBoardMembership {
181                granted_by: board.granted_by,
182                custom_role_id: board.custom_role_id,
183                board_id: board.board_id,
184                is_active: board.is_active,
185                is_admin: board.is_admin,
186                is_no_comments: board.is_no_comments,
187                is_comment_only: board.is_comment_only,
188                is_worker: board.is_worker,
189                is_normal_assigned_only: board.is_normal_assigned_only,
190                is_comment_assigned_only: board.is_comment_assigned_only,
191                is_read_only: board.is_read_only,
192                is_read_assigned_only: board.is_read_assigned_only,
193            })
194            .collect(),
195    }
196}
197
198fn non_empty(value: &str) -> Result<String, String> {
199    if value.is_empty() {
200        Err("value must not be empty".to_owned())
201    } else {
202        Ok(value.to_owned())
203    }
204}
205
206fn user_workspace_node(node: crate::client::UserWorkspaceNode) -> UserWorkspaceNodeDetail {
207    UserWorkspaceNodeDetail {
208        id: node.id,
209        name: node.name,
210        icon: node.icon,
211        children: node
212            .children
213            .map(|nodes| nodes.into_iter().map(user_workspace_node).collect()),
214    }
215}
216
217fn user_dialog_options(
218    dialogs: std::collections::BTreeMap<String, crate::client::UserDialogOptions>,
219) -> std::collections::BTreeMap<String, UserDialogOptionsDetail> {
220    dialogs
221        .into_iter()
222        .map(|(board, options)| {
223            (
224                board,
225                UserDialogOptionsDetail {
226                    board_id: options.board_id,
227                    swimlane_id: options.swimlane_id,
228                    list_id: options.list_id,
229                    card_id: options.card_id,
230                },
231            )
232        })
233        .collect()
234}
235
236fn user_profile(profile: crate::client::UserProfile) -> UserProfileDetail {
237    UserProfileDetail {
238        chat_view_layout: profile.chat_view_layout,
239        email_to_card: profile.email_to_card,
240        notification_delivery: profile.notification_delivery,
241        my_dependencies: profile.my_dependencies,
242        notify_muted_activities: profile.notify_muted_activities,
243        notify_override_tray: profile.notify_override_tray,
244        notify_override_email: profile.notify_override_email,
245        webhook_hide_identity: profile.webhook_hide_identity,
246        unread_changes_since: profile.unread_changes_since,
247        keep_title_tags: profile.keep_title_tags,
248        checklist_ding_sound: profile.checklist_ding_sound,
249        dismissed_board_announcements: profile.dismissed_board_announcements,
250        card_label_text_override: profile.card_label_text_override,
251        show_label_text_override: profile.show_label_text_override,
252        board_labels: profile.board_labels,
253        hidden_boards: profile.hidden_boards,
254        starred_swimlanes: profile.starred_swimlanes,
255        starred_lists: profile.starred_lists,
256        starred_cards: profile.starred_cards,
257        default_board_template_id: profile.default_board_template_id,
258        default_board_template_board_id: profile.default_board_template_board_id,
259        board_views: profile.board_views,
260        show_board_dependencies: profile.show_board_dependencies,
261        show_my_dependencies: profile.show_my_dependencies,
262        board_visits: profile.board_visits,
263        dismissed_rule_suggestions: profile.dismissed_rule_suggestions,
264        date_format_override: profile.date_format_override,
265        full_name: profile.fullname,
266        avatar_url: profile.avatar_url,
267        email_buffer: profile.email_buffer,
268        show_desktop_drag_handles: profile.show_desktop_drag_handles,
269        left_menu_collapsed: profile.left_menu_collapsed,
270        collapsed_workspaces: profile.collapsed_workspaces,
271        left_menu_width: profile.left_menu_width,
272        submit_on_enter: profile.submit_on_enter,
273        open_many_cards_at_once: profile.open_many_cards_at_once,
274        all_boards_theme_tiles: profile.all_boards_theme_tiles,
275        global_theme_color: profile.global_theme_color,
276        global_theme_custom_colors: profile.global_theme_custom_colors,
277        ui_font: profile.ui_font,
278        ui_font_size: profile.ui_font_size,
279        ui_text_color: profile.ui_text_color,
280        ui_text_bg_color: profile.ui_text_bg_color,
281        dismissed_announcement_version: profile.dismissed_announcement_version,
282        card_maximized: profile.card_maximized,
283        card_collapsed: profile.card_collapsed,
284        show_activities: profile.show_activities,
285        custom_fields_grid: profile.custom_fields_grid,
286        trello_api_saved: profile.trello_api_saved,
287        hidden_minicard_label_text: profile.hidden_minicard_label_text,
288        initials: profile.initials,
289        board_workspaces_tree: profile
290            .board_workspaces_tree
291            .map(|nodes| nodes.into_iter().map(user_workspace_node).collect()),
292        board_workspace_assignments: profile.board_workspace_assignments,
293        invited_boards: profile.invited_boards,
294        language: profile.language,
295        map_provider: profile.map_provider,
296        move_and_copy_dialog: profile.move_and_copy_dialog.map(user_dialog_options),
297        move_checklist_dialog: profile.move_checklist_dialog.map(user_dialog_options),
298        copy_checklist_dialog: profile.copy_checklist_dialog.map(user_dialog_options),
299        notifications: profile.notifications.map(|items| {
300            items
301                .into_iter()
302                .map(|item| UserProfileNotificationDetail {
303                    activity: item.activity,
304                    read: item.read,
305                    show_at: item.show_at,
306                })
307                .collect()
308        }),
309        rescue_card_description: profile.rescue_card_description,
310        show_cards_count_at: profile.show_cards_count_at,
311        start_day_of_week: profile.start_day_of_week,
312        starred_boards: profile.starred_boards,
313        starred_pages: profile.starred_pages.map(|items| {
314            items
315                .into_iter()
316                .map(|item| UserProfileStarredPageDetail {
317                    url: item.url,
318                    title: item.title,
319                })
320                .collect()
321        }),
322        default_board_id: profile.default_board_id,
323        icode: profile.icode,
324        board_view: profile.board_view,
325        list_sort_by: profile.list_sort_by,
326        all_boards_sort_by: profile.all_boards_sort_by,
327        templates_board_id: profile.templates_board_id,
328        card_templates_swimlane_id: profile.card_templates_swimlane_id,
329        list_templates_swimlane_id: profile.list_templates_swimlane_id,
330        board_templates_swimlane_id: profile.board_templates_swimlane_id,
331        sidebar_width: profile.sidebar_width,
332        list_widths: profile.list_widths,
333        list_constraints: profile.list_constraints,
334        auto_width_boards: profile.auto_width_boards,
335        board_sort_index: profile.board_sort_index,
336        fixed_list_width_boards: profile.fixed_list_width_boards,
337        fixed_list_widths: profile.fixed_list_widths,
338        swimlane_heights: profile.swimlane_heights,
339        collapsed_lists: profile.collapsed_lists,
340        collapsed_swimlanes: profile.collapsed_swimlanes,
341        collapsed_card_sections: profile.collapsed_card_sections,
342        keyboard_shortcuts: profile.keyboard_shortcuts,
343        vertical_scrollbars: profile.vertical_scrollbars,
344        show_week_of_year: profile.show_week_of_year,
345        date_format: profile.date_format,
346        mobile_mode: profile.mobile_mode,
347        card_zoom: profile.card_zoom,
348        calendar_system: profile.calendar_system,
349        collapsed_cards: profile.collapsed_cards,
350        card_last_views: profile.card_last_views,
351    }
352}