tdm_server_rust/middleware/
auth.rs1use crate::{
14 app::AppState,
15 cache::get_auth_snapshot_cached,
16 common::result::permission_denied_response,
17 entity::{
18 enums::{MemberInternEnum, PostEnum},
19 member::Member,
20 },
21 error::AppError,
22 utils::jwt::{JwtClaims, JwtUtil},
23};
24use axum::{
25 body::Body,
26 extract::State,
27 http::{Method, Request},
28 middleware::Next,
29 response::Response,
30};
31
32#[derive(Debug, Clone)]
34pub struct AuthMember(pub Option<Member>);
35
36#[tracing::instrument(skip_all, level = "info")]
38pub async fn auth_middleware(
39 State(state): State<AppState>,
40 mut req: Request<Body>,
41 next: Next,
42) -> Result<Response, AppError> {
43 let uri = req.uri().path().to_string();
44 let method = req.method().clone();
45
46 if is_public_auth_path(&uri) {
47 req.extensions_mut().insert(AuthMember(None));
48 return Ok(next.run(req).await);
49 }
50
51 if method == Method::OPTIONS {
52 req.extensions_mut().insert(AuthMember(None));
53 return Ok(next.run(req).await);
54 }
55
56 if uri.contains("undefined") {
57 return Err(AppError::login_expired("未登录哦 快加入提灯喵接坑吧喵!"));
58 }
59
60 let token = extract_token(req.headers());
61 if method != Method::GET && token.is_none() {
62 return Err(AppError::login_expired("请登录一下哦喵……"));
63 }
64
65 let mut member: Option<Member> = None;
66 if let Some(jwt) = token.as_deref() {
67 let claims = parse_token(&state, jwt)?;
68 member = Some(load_member(&state, claims.id).await?);
69 }
70
71 if requires_reviewer_permission(&uri, &method)
72 && !member.as_ref().is_some_and(Member::has_reviewer_permission)
73 {
74 return Ok(permission_denied_response());
75 }
76
77 check_route_permission(&uri, token.as_deref())?;
78
79 if uri.starts_with("/api/evaluations") {
80 if let Some(ref m) = member {
81 let posts = &m.post_ids;
82 let has = posts.contains(&2) || posts.contains(&4);
83 if !has && !posts.contains(&4) {
84 return Ok(permission_denied_response());
85 }
86 } else {
87 return Ok(permission_denied_response());
88 }
89 }
90
91 req.extensions_mut().insert(AuthMember(member));
92 Ok(next.run(req).await)
93}
94
95fn requires_reviewer_permission(uri: &str, method: &Method) -> bool {
99 if uri.starts_with("/api/admin/") {
100 return true;
101 }
102
103 let path = uri.trim_end_matches('/');
104 match *method {
105 Method::POST | Method::PUT | Method::DELETE => {
106 path == "/api/mangas"
107 || path == "/api/members"
108 || path == "/api/authors"
109 || path == "/api/magazines"
110 || path == "/api/episodes"
111 || path == "/api/mangas/station/admin"
112 || is_numeric_child(path, "/api/mangas/")
113 || is_numeric_child(path, "/api/members/")
114 || is_numeric_child(path, "/api/authors/")
115 || is_numeric_child(path, "/api/magazines/")
116 || is_numeric_child(path, "/api/episodes/")
117 }
118 _ => false,
119 }
120}
121
122pub(crate) fn member_can_upload_for_post(member: &Member, post_name: &str) -> bool {
127 if !MemberInternEnum::is_working_member(member.intern) {
128 return false;
129 }
130 let required = match post_name.to_ascii_lowercase().as_str() {
131 "provider" => PostEnum::PROVIDER,
132 "translator" => PostEnum::TRANSLATOR,
133 "proofreader" => PostEnum::PROOFREADER,
134 "letterer" => PostEnum::LETTERER,
135 "timer" => PostEnum::TIMER,
136 _ => return false,
137 };
138 member.intern == MemberInternEnum::REGULAR
139 || member.has_reviewer_permission()
140 || member.post_ids.contains(&required)
141}
142
143fn is_numeric_child(path: &str, prefix: &str) -> bool {
145 path.strip_prefix(prefix)
146 .is_some_and(|id| !id.is_empty() && id.bytes().all(|byte| byte.is_ascii_digit()))
147}
148
149#[tracing::instrument(name = "auth::parse_token", skip(state, token), level = "info")]
151fn parse_token(state: &AppState, token: &str) -> Result<JwtClaims, AppError> {
152 let util = JwtUtil::new(&state.config.jwt.sign_key, state.config.jwt.expire_ms);
153 util.parse(token).map_err(|e| {
154 if e.to_string().contains("ExpiredSignature") {
155 AppError::login_expired("登录数据过期,请重新登录喵!")
156 } else {
157 AppError::login_expired("Token解析失败喵!请重新登录喵!")
158 }
159 })
160}
161
162#[tracing::instrument(name = "auth::load_member", skip(state), level = "info")]
164async fn load_member(state: &AppState, member_id: i32) -> Result<Member, AppError> {
165 let m = get_auth_snapshot_cached(state, member_id).await?;
166 if crate::entity::enums::MemberInternEnum::is_left(m.intern) {
167 return Err(AppError::login_expired("你已经退出了提灯喵猫娘化计划喵……"));
168 }
169 Ok(m)
170}
171
172#[tracing::instrument(name = "auth::check_permission", skip(uri, token), level = "info")]
174fn check_route_permission(uri: &str, token: Option<&str>) -> Result<(), AppError> {
175 let needs_strict =
176 uri.contains("takeEpisode") || uri.contains("invitationCodes") || uri.contains("download");
177 if needs_strict && token.is_none() {
178 return Err(AppError::login_expired("必须要登录才能访问这里喵!"));
179 }
180 Ok(())
181}
182
183fn is_public_auth_path(uri: &str) -> bool {
185 matches!(uri, "/api/login" | "/api/reg" | "/login" | "/reg")
186}
187
188fn extract_token(headers: &axum::http::HeaderMap) -> Option<String> {
190 headers
191 .get("token")
192 .or_else(|| headers.get("Authorization"))
193 .and_then(|v| v.to_str().ok())
194 .map(|s| s.to_string())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::{is_public_auth_path, member_can_upload_for_post, requires_reviewer_permission};
200 use crate::entity::{
201 enums::{MemberInternEnum, PostEnum},
202 member::Member,
203 };
204 use axum::http::Method;
205
206 #[test]
207 fn public_auth_path_only_login_and_reg() {
208 assert!(is_public_auth_path("/api/login"));
209 assert!(is_public_auth_path("/api/reg"));
210 assert!(is_public_auth_path("/login"));
211 assert!(is_public_auth_path("/reg"));
212 assert!(!is_public_auth_path(
213 "/api/editor/episodes/1/pages/register"
214 ));
215 assert!(!is_public_auth_path("/editor/episodes/1/pages/register"));
216 assert!(!is_public_auth_path("/api/members/18"));
217 }
218
219 #[test]
220 fn admin_pages_and_management_writes_require_reviewer() {
221 assert!(requires_reviewer_permission(
222 "/api/admin/taskTracking/episodeTasks",
223 &Method::GET
224 ));
225 assert!(requires_reviewer_permission("/api/members", &Method::PUT));
226 assert!(requires_reviewer_permission(
227 "/api/mangas/42",
228 &Method::DELETE
229 ));
230 assert!(requires_reviewer_permission(
231 "/api/mangas/station/admin",
232 &Method::POST
233 ));
234 }
235
236 #[test]
237 fn ordinary_member_actions_are_not_treated_as_admin_writes() {
238 assert!(!requires_reviewer_permission(
239 "/api/members/self",
240 &Method::PUT
241 ));
242 assert!(!requires_reviewer_permission(
243 "/api/members/submitEpisode",
244 &Method::PATCH
245 ));
246 assert!(!requires_reviewer_permission(
247 "/api/mangas/collect",
248 &Method::POST
249 ));
250 assert!(!requires_reviewer_permission(
251 "/api/episodes/rollback/42/translator",
252 &Method::PUT
253 ));
254 }
255
256 #[test]
257 fn regular_members_can_upload_to_any_supported_post() {
258 let mut member = Member::default();
259 assert!(member_can_upload_for_post(&member, "provider"));
260 assert!(member_can_upload_for_post(&member, "translator"));
261 assert!(member_can_upload_for_post(&member, "proofreader"));
262 assert!(member_can_upload_for_post(&member, "letterer"));
263 assert!(member_can_upload_for_post(&member, "timer"));
264
265 member.intern = MemberInternEnum::INTERN;
266 member.post_ids = vec![PostEnum::TRANSLATOR];
267 assert!(member_can_upload_for_post(&member, "translator"));
268 assert!(!member_can_upload_for_post(&member, "provider"));
269
270 member.post_ids = vec![PostEnum::REVIEWER];
271 assert!(member_can_upload_for_post(&member, "provider"));
272 assert!(!member_can_upload_for_post(&member, "../../backup"));
273
274 member.intern = MemberInternEnum::RETIRED;
275 assert!(!member.has_reviewer_permission());
276 assert!(!member_can_upload_for_post(&member, "translator"));
277 }
278}