1use std::path::PathBuf;
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use serde::Serialize;
6
7#[derive(Debug, thiserror::Error)]
8pub enum Error {
9 #[error("config not found (searched: {searched:?})")]
11 ConfigNotFound { searched: Vec<PathBuf> },
12
13 #[error("config parse error in {path}: {source}")]
14 ConfigParse {
15 path: PathBuf,
16 source: toml::de::Error,
17 },
18
19 #[error("config error: {0}")]
20 Config(String),
21
22 #[error("database error: {0}")]
24 Database(#[from] sqlx::Error),
25
26 #[error("database migration error: {0}")]
27 Migration(#[from] sqlx::migrate::MigrateError),
28
29 #[error("invalid or expired token")]
31 InvalidToken,
32
33 #[error("token expired")]
34 ExpiredToken,
35
36 #[error("missing authentication")]
37 Unauthenticated,
38
39 #[error("insufficient permissions")]
40 Forbidden,
41
42 #[error("OAuth error: {0}")]
43 OAuth(String),
44
45 #[error("invalid OAuth state")]
46 InvalidOAuthState,
47
48 #[error("user not found")]
50 UserNotFound,
51
52 #[error("username taken")]
53 UsernameTaken,
54
55 #[error("username held until {held_until}")]
56 UsernameHeld { held_until: chrono::DateTime<chrono::Utc> },
57
58 #[error("username change on cooldown until {available_at}")]
59 UsernameChangeCooldown {
60 available_at: chrono::DateTime<chrono::Utc>,
61 },
62
63 #[error("invalid username: {reason}")]
64 InvalidUsername { reason: String },
65
66 #[error("reserved username")]
67 ReservedUsername,
68
69 #[error("cannot unlink last provider")]
70 LastProvider,
71
72 #[error("provider already linked")]
73 ProviderAlreadyLinked,
74
75 #[error("invalid client")]
77 InvalidClient,
78
79 #[error("invalid redirect URI")]
80 InvalidRedirectUri,
81
82 #[error("invalid authorization code")]
83 InvalidAuthorizationCode,
84
85 #[error("invalid grant")]
86 InvalidGrant,
87
88 #[error("unsupported grant type")]
89 UnsupportedGrantType,
90
91 #[error("invalid scope")]
92 InvalidScope,
93
94 #[error("consent required")]
95 ConsentRequired,
96
97 #[error("bad request: {0}")]
99 BadRequest(String),
100
101 #[error("not found")]
102 NotFound,
103
104 #[error("payload too large")]
105 PayloadTooLarge,
106
107 #[error("unsupported media type")]
108 UnsupportedMediaType,
109
110 #[error("rate limited")]
111 RateLimited,
112
113 #[error(transparent)]
114 Internal(#[from] anyhow::Error),
115}
116
117#[derive(Serialize, utoipa::ToSchema)]
118pub struct ErrorBody {
119 error: String,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 error_description: Option<String>,
124}
125
126impl Error {
127 fn status_code(&self) -> StatusCode {
128 match self {
129 Self::ConfigNotFound { .. }
130 | Self::ConfigParse { .. }
131 | Self::Config(_)
132 | Self::Migration(_) => StatusCode::INTERNAL_SERVER_ERROR,
133
134 Self::Database(_) | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
135
136 Self::InvalidToken | Self::ExpiredToken | Self::Unauthenticated => StatusCode::UNAUTHORIZED,
137 Self::Forbidden => StatusCode::FORBIDDEN,
138
139 Self::OAuth(_) | Self::InvalidOAuthState => StatusCode::BAD_REQUEST,
140
141 Self::UserNotFound | Self::NotFound => StatusCode::NOT_FOUND,
142
143 Self::InvalidClient => StatusCode::UNAUTHORIZED,
144
145 Self::UsernameTaken | Self::ProviderAlreadyLinked => StatusCode::CONFLICT,
146
147 Self::UsernameHeld { .. }
148 | Self::UsernameChangeCooldown { .. }
149 | Self::InvalidUsername { .. }
150 | Self::ReservedUsername
151 | Self::LastProvider
152 | Self::InvalidRedirectUri
153 | Self::InvalidAuthorizationCode
154 | Self::InvalidGrant
155 | Self::UnsupportedGrantType
156 | Self::InvalidScope
157 | Self::ConsentRequired
158 | Self::BadRequest(_) => StatusCode::BAD_REQUEST,
159
160 Self::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
161 Self::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
162 Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
163 }
164 }
165
166 fn error_code(&self) -> &'static str {
168 match self {
169 Self::ConfigNotFound { .. } => "config_not_found",
170 Self::ConfigParse { .. } => "config_parse_error",
171 Self::Config(_) => "config_error",
172 Self::Database(_) => "internal_error",
173 Self::Migration(_) => "migration_error",
174 Self::InvalidToken => "invalid_token",
175 Self::ExpiredToken => "invalid_token",
176 Self::Unauthenticated => "unauthenticated",
177 Self::Forbidden => "forbidden",
178 Self::OAuth(_) => "oauth_error",
179 Self::InvalidOAuthState => "invalid_oauth_state",
180 Self::UserNotFound => "user_not_found",
181 Self::UsernameTaken => "username_taken",
182 Self::UsernameHeld { .. } => "username_held",
183 Self::UsernameChangeCooldown { .. } => "username_change_cooldown",
184 Self::InvalidUsername { .. } => "invalid_username",
185 Self::ReservedUsername => "reserved_username",
186 Self::LastProvider => "last_provider",
187 Self::ProviderAlreadyLinked => "provider_already_linked",
188 Self::InvalidClient => "invalid_client",
189 Self::InvalidRedirectUri => "invalid_redirect_uri",
190 Self::InvalidAuthorizationCode => "invalid_grant",
191 Self::InvalidGrant => "invalid_grant",
192 Self::UnsupportedGrantType => "unsupported_grant_type",
193 Self::InvalidScope => "invalid_scope",
194 Self::ConsentRequired => "consent_required",
195 Self::BadRequest(_) => "bad_request",
196 Self::NotFound => "not_found",
197 Self::PayloadTooLarge => "payload_too_large",
198 Self::UnsupportedMediaType => "unsupported_media_type",
199 Self::RateLimited => "rate_limited",
200 Self::Internal(_) => "internal_error",
201 }
202 }
203}
204
205impl IntoResponse for Error {
206 fn into_response(self) -> Response {
207 let status = self.status_code();
208
209 let error_description = if status.is_server_error() {
211 tracing::error!(error = %self, "internal error");
212 None
213 } else {
214 Some(self.to_string())
215 };
216
217 let body = ErrorBody {
218 error: self.error_code().to_string(),
219 error_description,
220 };
221
222 (status, axum::Json(body)).into_response()
223 }
224}
225
226pub fn is_unique_violation(err: &Error) -> bool {
228 if let Error::Database(sqlx::Error::Database(db_err)) = err {
229 return db_err.code().as_deref() == Some("23505");
230 }
231 false
232}
233
234pub fn unique_violation_constraint(err: &Error) -> Option<String> {
236 if let Error::Database(sqlx::Error::Database(db_err)) = err {
237 if db_err.code().as_deref() == Some("23505") {
238 return db_err.constraint().map(|s| s.to_string());
239 }
240 }
241 None
242}
243
244pub fn www_authenticate_value(issuer: &str, error: &Error) -> Option<String> {
249 let realm = issuer.replace('\\', "\\\\").replace('"', "\\\"");
251 match error {
252 Error::Unauthenticated => {
253 Some(format!("Bearer realm=\"{realm}\""))
254 }
255 Error::ExpiredToken => {
256 Some(format!(
257 "Bearer realm=\"{realm}\", error=\"invalid_token\", error_description=\"token expired\""
258 ))
259 }
260 Error::InvalidToken => {
261 Some(format!(
262 "Bearer realm=\"{realm}\", error=\"invalid_token\""
263 ))
264 }
265 Error::Forbidden => {
266 Some(format!(
267 "Bearer realm=\"{realm}\", error=\"insufficient_scope\""
268 ))
269 }
270 _ => None,
271 }
272}
273
274pub type Result<T> = std::result::Result<T, Error>;