1use std::sync::atomic::Ordering;
4
5use axum::Json;
6use axum::extract::{Path, State};
7use axum::http::StatusCode as ReqwestStatusCode;
8use axum::response::{IntoResponse as _, Response};
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12use crate::auth::CurrentUser;
13use crate::error::Error;
14use crate::groups::{self, GroupMemberView, GroupRole, GroupView};
15use crate::library;
16use crate::rate_limit::{self, RateCategory};
17use crate::state::AppState;
18
19#[derive(Debug, Deserialize)]
21pub struct CreateGroupRequest {
22 pub name: String,
24}
25
26#[derive(Debug, Deserialize)]
28pub struct RenameGroupRequest {
29 pub name: String,
31}
32
33#[derive(Debug, Serialize)]
35pub struct GroupResponse {
36 pub group: GroupView,
38}
39
40#[derive(Debug, Serialize)]
42pub struct ListGroupsResponse {
43 pub groups: Vec<GroupView>,
45}
46
47#[derive(Debug, Serialize)]
49pub struct ListMembersResponse {
50 pub members: Vec<GroupMemberView>,
52}
53
54#[derive(Debug, Deserialize)]
56pub struct SetRoleRequest {
57 pub role: String,
59}
60
61pub async fn list_mine(
67 user: CurrentUser,
68 State(state): State<AppState>,
69) -> Result<Json<ListGroupsResponse>, Error> {
70 let groups = groups::list_for_user(&state.db, user.user_id).await?;
71 Ok(Json(ListGroupsResponse { groups }))
72}
73
74pub async fn create(
82 user: CurrentUser,
83 State(state): State<AppState>,
84 Json(req): Json<CreateGroupRequest>,
85) -> Result<(ReqwestStatusCode, Json<GroupResponse>), Error> {
86 rate_limit::try_acquire(&state.db, RateCategory::GroupCreate, user.user_id).await?;
87 let name = library::sanitise_display_name(&req.name, "group name")?;
88 let group_id = groups::create_group(&state.db, &name, user.user_id).await?;
89 let group = groups::get_for_user(&state.db, group_id, user.user_id).await?;
90 Ok((ReqwestStatusCode::CREATED, Json(GroupResponse { group })))
91}
92
93pub async fn get(
100 user: CurrentUser,
101 State(state): State<AppState>,
102 Path(group_id): Path<Uuid>,
103) -> Result<Json<GroupResponse>, Error> {
104 let group = groups::get_for_user(&state.db, group_id, user.user_id).await?;
105 Ok(Json(GroupResponse { group }))
106}
107
108pub async fn rename(
115 user: CurrentUser,
116 State(state): State<AppState>,
117 Path(group_id): Path<Uuid>,
118 Json(req): Json<RenameGroupRequest>,
119) -> Result<Json<GroupResponse>, Error> {
120 require_owner(&state, group_id, user.user_id).await?;
121 let name = library::sanitise_display_name(&req.name, "group name")?;
122 groups::rename_group(&state.db, group_id, &name).await?;
123 let group = groups::get_for_user(&state.db, group_id, user.user_id).await?;
124 Ok(Json(GroupResponse { group }))
125}
126
127pub async fn delete(
137 user: CurrentUser,
138 State(state): State<AppState>,
139 Path(group_id): Path<Uuid>,
140) -> Result<Response, Error> {
141 require_owner(&state, group_id, user.user_id).await?;
142 groups::delete_group(&state.db, group_id).await?;
143 state.library_cleanup_dirty.store(true, Ordering::Release);
144 Ok((ReqwestStatusCode::NO_CONTENT, "").into_response())
145}
146
147pub async fn list_members(
154 user: CurrentUser,
155 State(state): State<AppState>,
156 Path(group_id): Path<Uuid>,
157) -> Result<Json<ListMembersResponse>, Error> {
158 require_member(&state, group_id, user.user_id).await?;
159 let members = groups::list_members(&state.db, group_id).await?;
160 Ok(Json(ListMembersResponse { members }))
161}
162
163pub async fn set_member_role(
175 user: CurrentUser,
176 State(state): State<AppState>,
177 Path((group_id, target_user_id)): Path<(Uuid, Uuid)>,
178 Json(req): Json<SetRoleRequest>,
179) -> Result<Response, Error> {
180 require_owner(&state, group_id, user.user_id).await?;
181 let role = GroupRole::parse(req.role.trim())?;
182 let current = groups::lookup_role(&state.db, group_id, target_user_id)
183 .await?
184 .ok_or_else(|| {
185 Error::NotFound(format!(
186 "user {target_user_id} is not a member of group {group_id}"
187 ))
188 })?;
189 match (current, role) {
190 (GroupRole::Owner, GroupRole::Owner) | (GroupRole::Member, GroupRole::Member) => {}
191 (GroupRole::Member, GroupRole::Owner) => {
192 let promoted =
193 groups::try_promote_member_to_owner(&state.db, group_id, target_user_id).await?;
194 if !promoted {
195 return Err(Error::NotFound(format!(
196 "user {target_user_id} is not a member of group {group_id}"
197 )));
198 }
199 }
200 (GroupRole::Owner, GroupRole::Member) => {
201 if target_user_id != user.user_id {
202 return Err(Error::Forbidden(
203 "owners cannot demote other owners; ask them to demote themselves or leave"
204 .to_owned(),
205 ));
206 }
207 let demoted = groups::try_self_demote_owner(&state.db, group_id, user.user_id).await?;
208 if !demoted {
209 return Err(Error::BadRequest(
210 "cannot demote the last owner; promote another member first or delete the group"
211 .to_owned(),
212 ));
213 }
214 }
215 }
216 Ok((ReqwestStatusCode::NO_CONTENT, "").into_response())
217}
218
219pub async fn remove_member(
230 user: CurrentUser,
231 State(state): State<AppState>,
232 Path((group_id, target_user_id)): Path<(Uuid, Uuid)>,
233) -> Result<Response, Error> {
234 require_owner(&state, group_id, user.user_id).await?;
235 let removed = groups::try_remove_non_owner(&state.db, group_id, target_user_id).await?;
236 if removed {
237 return Ok((ReqwestStatusCode::NO_CONTENT, "").into_response());
238 }
239 match groups::lookup_role(&state.db, group_id, target_user_id).await? {
240 None => Err(Error::NotFound(format!(
241 "user {target_user_id} is not a member of group {group_id}"
242 ))),
243 Some(_) => Err(Error::Forbidden(
244 "owners cannot be removed by other owners; the owner must demote themselves or leave"
245 .to_owned(),
246 )),
247 }
248}
249
250pub async fn leave(
258 user: CurrentUser,
259 State(state): State<AppState>,
260 Path(group_id): Path<Uuid>,
261) -> Result<Response, Error> {
262 let left = groups::try_leave(&state.db, group_id, user.user_id).await?;
263 if left {
264 return Ok((ReqwestStatusCode::NO_CONTENT, "").into_response());
265 }
266 match groups::lookup_role(&state.db, group_id, user.user_id).await? {
267 None => Err(Error::NotFound(format!(
268 "you are not a member of group {group_id}"
269 ))),
270 Some(_) => Err(Error::BadRequest(
271 "you are the last owner of this group; promote another member to owner or \
272 delete the group instead of leaving"
273 .to_owned(),
274 )),
275 }
276}
277
278async fn require_owner(state: &AppState, group_id: Uuid, user_id: Uuid) -> Result<(), Error> {
282 if groups::lookup_role(&state.db, group_id, user_id).await? == Some(GroupRole::Owner) {
283 Ok(())
284 } else {
285 Err(Error::NotFound(format!("group {group_id}")))
286 }
287}
288
289async fn require_member(state: &AppState, group_id: Uuid, user_id: Uuid) -> Result<(), Error> {
293 if groups::lookup_role(&state.db, group_id, user_id)
294 .await?
295 .is_some()
296 {
297 Ok(())
298 } else {
299 Err(Error::NotFound(format!("group {group_id}")))
300 }
301}