1use axum::extract::{Query, State};
9use serde::{Deserialize, Serialize};
10use utoipa::ToSchema;
11
12use super::AppState;
13use crate::error::ApiError;
14use crate::middleware::SignerContext;
15use crate::sonic_json::SonicJson;
16use crate::username_service::UsernameServiceError;
17
18#[derive(Debug, Deserialize)]
23pub struct UsernameQuery {
24 pub wallet: Option<String>,
25 pub username: Option<String>,
26}
27
28#[derive(Debug, Deserialize, ToSchema)]
29pub struct SetUsernameRequest {
30 pub username: String,
31}
32
33#[derive(Debug, Serialize, ToSchema)]
34pub struct UsernameResponse {
35 pub wallet_address: String,
36 pub username: String,
37}
38
39#[derive(Debug, Serialize, ToSchema)]
40pub struct DeleteUsernameResponse {
41 pub deleted: bool,
42}
43
44#[utoipa::path(
50 get,
51 path = "/username",
52 params(
53 ("wallet" = Option<String>, Query, description = "Wallet address to look up"),
54 ("username" = Option<String>, Query, description = "Username to reverse-look up"),
55 ),
56 responses(
57 (status = 200, description = "Username found", body = UsernameResponse),
58 (status = 400, description = "Must provide wallet or username query param"),
59 (status = 404, description = "No username found"),
60 ),
61 tag = "Username"
62)]
63pub async fn get_username(
64 State(state): State<AppState>,
65 Query(query): Query<UsernameQuery>,
66) -> Result<SonicJson<UsernameResponse>, ApiError> {
67 let svc = &state.username_service;
68
69 if let Some(wallet) = &query.wallet {
70 let row = svc.get_by_wallet(wallet).await.map_err(|e| {
71 tracing::error!("get_username by wallet failed: {e}");
72 ApiError::internal_error("Failed to look up username")
73 })?;
74
75 match row {
76 Some(r) => Ok(SonicJson(UsernameResponse {
77 wallet_address: r.wallet_address,
78 username: r.username,
79 })),
80 None => Err(ApiError::not_found("No username set for this wallet")),
81 }
82 } else if let Some(username) = &query.username {
83 let row = svc.get_by_username(username).await.map_err(|e| {
84 tracing::error!("get_username by name failed: {e}");
85 ApiError::internal_error("Failed to look up wallet")
86 })?;
87
88 match row {
89 Some(r) => Ok(SonicJson(UsernameResponse {
90 wallet_address: r.wallet_address,
91 username: r.username,
92 })),
93 None => Err(ApiError::not_found("No wallet found for this username")),
94 }
95 } else {
96 Err(ApiError::bad_request(
97 "Must provide either 'wallet' or 'username' query parameter",
98 ))
99 }
100}
101
102#[utoipa::path(
104 post,
105 path = "/username",
106 request_body = SetUsernameRequest,
107 responses(
108 (status = 200, description = "Username set", body = UsernameResponse),
109 (status = 400, description = "Validation error"),
110 (status = 409, description = "Username already taken"),
111 (status = 500, description = "Internal server error"),
112 ),
113 security(("eip712_signature" = [])),
114 tag = "Username"
115)]
116pub async fn set_username(
117 State(state): State<AppState>,
118 signer_ctx: SignerContext,
119 SonicJson(request): SonicJson<SetUsernameRequest>,
120) -> Result<SonicJson<UsernameResponse>, ApiError> {
121 let wallet = format!("{:#x}", signer_ctx.wallet_address.0);
122
123 tracing::info!(wallet = %wallet, "set_username request");
124 tracing::debug!(wallet = %wallet, username = %request.username, "set_username details");
125
126 crate::username_service::validate_username(&request.username).map_err(ApiError::bad_request)?;
128
129 let existing = state
131 .username_service
132 .get_by_username(&request.username)
133 .await
134 .map_err(|e| {
135 tracing::error!("set_username uniqueness check failed: {e}");
136 ApiError::internal_error("Failed to check username availability")
137 })?;
138
139 if let Some(ref row) = existing {
140 if row.wallet_address.to_lowercase() != wallet.to_lowercase() {
141 return Err(ApiError::new(
142 axum::http::StatusCode::CONFLICT,
143 "username_taken",
144 "This username is already taken",
145 ));
146 }
147 }
148
149 let row = state
150 .username_service
151 .set_username(&wallet, &request.username)
152 .await
153 .map_err(|e| match e {
154 UsernameServiceError::UniqueViolation => ApiError::new(
155 axum::http::StatusCode::CONFLICT,
156 "username_taken",
157 "This username is already taken",
158 ),
159 UsernameServiceError::Internal(inner) => {
160 tracing::error!("set_username failed: {inner}");
161 ApiError::internal_error("Failed to set username")
162 }
163 })?;
164
165 Ok(SonicJson(UsernameResponse {
166 wallet_address: row.wallet_address,
167 username: row.username,
168 }))
169}
170
171#[utoipa::path(
173 delete,
174 path = "/username",
175 responses(
176 (status = 200, description = "Username deleted", body = DeleteUsernameResponse),
177 (status = 500, description = "Internal server error"),
178 ),
179 security(("eip712_signature" = [])),
180 tag = "Username"
181)]
182pub async fn delete_username(
183 State(state): State<AppState>,
184 signer_ctx: SignerContext,
185) -> Result<SonicJson<DeleteUsernameResponse>, ApiError> {
186 let wallet = format!("{:#x}", signer_ctx.wallet_address.0);
187
188 tracing::info!(wallet = %wallet, "delete_username request");
189
190 let deleted = state
191 .username_service
192 .delete_username(&wallet)
193 .await
194 .map_err(|e| {
195 tracing::error!("delete_username failed: {e}");
196 ApiError::internal_error("Failed to delete username")
197 })?;
198
199 Ok(SonicJson(DeleteUsernameResponse { deleted }))
200}