1use axum::Json;
2use axum::extract::{Path, State};
3use kellnr_common::account_name::{GroupName, UserName};
4use kellnr_db::{self, Group, UserId};
5use kellnr_http::error::ApiError;
6use kellnr_http::identity::AdminUser;
7use kellnr_http::state::DbState;
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11#[utoipa::path(
13 get,
14 path = "/",
15 tag = "groups",
16 responses(
17 (status = 200, description = "List of all groups", body = Vec<Group>),
18 (status = 403, description = "Admin access required")
19 ),
20 security(("session_cookie" = []))
21)]
22pub async fn list_groups(
23 _user: AdminUser,
24 State(db): DbState,
25) -> Result<Json<Vec<Group>>, ApiError> {
26 Ok(Json(db.get_groups().await?))
27}
28
29#[utoipa::path(
31 delete,
32 path = "/{name}",
33 tag = "groups",
34 params(
35 ("name" = String, Path, description = "Group name to delete")
36 ),
37 responses(
38 (status = 200, description = "Group deleted successfully"),
39 (status = 403, description = "Admin access required")
40 ),
41 security(("session_cookie" = []))
42)]
43pub async fn delete(
44 _user: AdminUser,
45 Path(name): Path<GroupName>,
46 State(db): DbState,
47) -> Result<(), ApiError> {
48 Ok(db.delete_group(&name).await?)
49}
50
51#[derive(Deserialize, ToSchema)]
52pub struct NewGroup {
53 pub name: String,
54}
55
56impl NewGroup {
57 pub fn validate(&self) -> Result<GroupName, ApiError> {
59 Ok(GroupName::for_new_group(self.name.as_str())?)
60 }
61}
62
63#[utoipa::path(
65 post,
66 path = "/",
67 tag = "groups",
68 request_body = NewGroup,
69 responses(
70 (status = 200, description = "Group created successfully"),
71 (status = 400, description = "Validation failed"),
72 (status = 403, description = "Admin access required")
73 ),
74 security(("session_cookie" = []))
75)]
76pub async fn add(
77 _user: AdminUser,
78 State(db): DbState,
79 Json(new_group): Json<NewGroup>,
80) -> Result<(), ApiError> {
81 let name = new_group.validate()?;
82
83 Ok(db.add_group(&name).await?)
84}
85
86#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
87pub struct GroupUser {
88 pub id: UserId,
89 pub name: UserName,
90}
91
92#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
93pub struct GroupUserList {
94 pub users: Vec<GroupUser>,
95}
96
97impl From<Vec<GroupUser>> for GroupUserList {
98 fn from(users: Vec<GroupUser>) -> Self {
99 Self { users }
100 }
101}
102
103#[utoipa::path(
105 get,
106 path = "/{group_name}/members",
107 tag = "groups",
108 params(
109 ("group_name" = String, Path, description = "Group name")
110 ),
111 responses(
112 (status = 200, description = "List of group members", body = GroupUserList),
113 (status = 403, description = "Admin access required")
114 ),
115 security(("session_cookie" = []))
116)]
117pub async fn list_users(
118 _user: AdminUser,
119 Path(group_name): Path<GroupName>,
120 State(db): DbState,
121) -> Result<Json<GroupUserList>, ApiError> {
122 let users: Vec<GroupUser> = db
123 .get_group_users(&group_name)
124 .await?
125 .iter()
126 .map(|u| GroupUser {
127 id: u.id,
128 name: u.name.clone(),
129 })
130 .collect();
131
132 Ok(Json(GroupUserList::from(users)))
133}
134
135#[utoipa::path(
137 put,
138 path = "/{group_name}/members/{name}",
139 tag = "groups",
140 params(
141 ("group_name" = String, Path, description = "Group name"),
142 ("name" = String, Path, description = "Username to add")
143 ),
144 responses(
145 (status = 200, description = "User added to group successfully"),
146 (status = 403, description = "Admin access required")
147 ),
148 security(("session_cookie" = []))
149)]
150pub async fn add_user(
151 _user: AdminUser,
152 Path((group_name, name)): Path<(GroupName, UserName)>,
153 State(db): DbState,
154) -> Result<(), ApiError> {
155 if !db.is_group_user(&group_name, &name).await? {
156 db.add_group_user(&group_name, &name).await?;
157 }
158
159 Ok(())
160}
161
162#[utoipa::path(
164 delete,
165 path = "/{group_name}/members/{name}",
166 tag = "groups",
167 params(
168 ("group_name" = String, Path, description = "Group name"),
169 ("name" = String, Path, description = "Username to remove")
170 ),
171 responses(
172 (status = 200, description = "User removed from group successfully"),
173 (status = 403, description = "Admin access required")
174 ),
175 security(("session_cookie" = []))
176)]
177pub async fn delete_user(
178 _user: AdminUser,
179 Path((group_name, name)): Path<(GroupName, UserName)>,
180 State(db): DbState,
181) -> Result<(), ApiError> {
182 Ok(db.delete_group_user(&group_name, &name).await?)
183}
184
185#[cfg(test)]
186mod tests {
187 use axum::http::StatusCode;
188
189 use super::*;
190
191 #[tokio::test]
192 async fn add_group_with_invalid_name_is_bad_request() {
193 for name in ["", " ", "dev\tteam", &"a".repeat(65)] {
194 let result = add(
195 AdminUser(kellnr_http::test_support::session_identity("admin", true)),
196 State(kellnr_http::test_support::test_state().db),
197 Json(NewGroup {
198 name: name.to_string(),
199 }),
200 )
201 .await;
202
203 assert!(
204 result
205 .as_ref()
206 .is_err_and(|e| e.status() == StatusCode::BAD_REQUEST),
207 "{name:?}"
208 );
209 }
210 }
211}