1use crate::state::{
4 wants_json, FortifyState, PASSWORD_CONFIRMED_AT, PASSWORD_CONFIRM_TTL_SECS, PENDING_2FA_KEY,
5};
6use crate::store::CurrentUser;
7use crate::token::now_secs;
8use sova_core::extend::{named, MwEntry};
9use sova_core::{with_state, Error, IntoResponse, Next, Redirect, Request, Response, Result};
10use sova_passport::PassportExt;
11use sova_session::SessionExt;
12
13pub fn fortify_guard(login_path: impl Into<String>) -> MwEntry {
15 let login = login_path.into();
16 named(
17 "fortify-guard",
18 with_state(login, |login, req, next| async move {
19 if req.get::<CurrentUser>().is_some() || req.is_authenticated() {
20 if req.get::<CurrentUser>().is_none() {
21 return reject(&req, login.as_str()).into_response();
22 }
23 return next(req).await;
24 }
25 reject(&req, login.as_str()).into_response()
26 }),
27 )
28}
29
30pub fn fortify_guard_from_state() -> MwEntry {
32 named("fortify-guard", |req: Request, next: Next| async move {
33 let login = req
34 .try_state::<FortifyState>()
35 .map(|s| s.login_path.clone())
36 .unwrap_or_else(|| "/login".into());
37 if req.get::<CurrentUser>().is_some() || req.is_authenticated() {
38 if req.get::<CurrentUser>().is_none() {
39 return reject(&req, &login).into_response();
40 }
41 return next(req).await;
42 }
43 reject(&req, &login).into_response()
44 })
45}
46
47fn reject(req: &Request, login: &str) -> Response {
48 if wants_json(req) {
49 Error::Unauthorized.into_response()
50 } else {
51 Redirect::see_other(login).into_response()
52 }
53}
54
55pub fn verified(verify_path: impl Into<String>) -> MwEntry {
57 let path = verify_path.into();
58 named(
59 "fortify-verified",
60 with_state(path, |path, req, next| async move {
61 match req.get::<CurrentUser>() {
62 Some(u) if u.email_verified => next(req).await,
63 Some(_) => {
64 if wants_json(&req) {
65 Error::custom(403, "Email not verified").into_response()
66 } else {
67 Redirect::see_other(path.as_str()).into_response()
68 }
69 }
70 None => Error::Unauthorized.into_response(),
71 }
72 }),
73 )
74}
75
76pub fn verified_from_state() -> MwEntry {
77 named("fortify-verified", |req: Request, next: Next| async move {
78 let path = req
79 .try_state::<FortifyState>()
80 .map(|s| s.verify_path.clone())
81 .unwrap_or_else(|| "/email/verify".into());
82 match req.get::<CurrentUser>() {
83 Some(u) if u.email_verified => next(req).await,
84 Some(_) => {
85 if wants_json(&req) {
86 Error::custom(403, "Email not verified").into_response()
87 } else {
88 Redirect::see_other(path.as_str()).into_response()
89 }
90 }
91 None => Error::Unauthorized.into_response(),
92 }
93 })
94}
95
96pub fn password_confirmed(confirm_path: impl Into<String>) -> MwEntry {
98 let path = confirm_path.into();
99 named(
100 "fortify-password-confirmed",
101 with_state(path, |path, req, next| async move {
102 if is_password_confirmed(&req) {
103 return next(req).await;
104 }
105 if wants_json(&req) {
106 Error::custom(423, "Password confirmation required").into_response()
107 } else {
108 Redirect::see_other(path.as_str()).into_response()
109 }
110 }),
111 )
112}
113
114pub fn password_confirmed_from_state() -> MwEntry {
115 named(
116 "fortify-password-confirmed",
117 |req: Request, next: Next| async move {
118 let path = req
119 .try_state::<FortifyState>()
120 .map(|s| s.confirm_password_path.clone())
121 .unwrap_or_else(|| "/user/confirm-password".into());
122 if is_password_confirmed(&req) {
123 return next(req).await;
124 }
125 if wants_json(&req) {
126 Error::custom(423, "Password confirmation required").into_response()
127 } else {
128 Redirect::see_other(path.as_str()).into_response()
129 }
130 },
131 )
132}
133
134pub fn is_password_confirmed(req: &Request) -> bool {
135 let Some(raw) = req.session().get(PASSWORD_CONFIRMED_AT) else {
136 return false;
137 };
138 let Ok(at) = raw.parse::<u64>() else {
139 return false;
140 };
141 now_secs().saturating_sub(at) <= PASSWORD_CONFIRM_TTL_SECS
142}
143
144pub fn mark_password_confirmed(req: &Request) {
145 req.session()
146 .set(PASSWORD_CONFIRMED_AT, now_secs().to_string());
147}
148
149pub fn permission(slug: impl Into<String>) -> MwEntry {
151 let slug = slug.into();
152 named(
153 format!("fortify-permission:{slug}"),
154 with_state(slug, |slug, req, next| async move {
155 match req.get::<CurrentUser>() {
156 Some(u) if u.has_permission(&slug) => next(req).await,
157 Some(_) => Error::custom(403, "Forbidden").into_response(),
158 None => Error::Unauthorized.into_response(),
159 }
160 }),
161 )
162}
163
164pub fn role(slug: impl Into<String>) -> MwEntry {
166 let slug = slug.into();
167 named(
168 format!("fortify-role:{slug}"),
169 with_state(slug, |slug, req, next| async move {
170 match req.get::<CurrentUser>() {
171 Some(u) if u.has_role(&slug) => next(req).await,
172 Some(_) => Error::custom(403, "Forbidden").into_response(),
173 None => Error::Unauthorized.into_response(),
174 }
175 }),
176 )
177}
178
179pub trait AuthExt {
181 fn current_user(&self) -> Option<&CurrentUser>;
182 fn require_current_user(&self) -> Result<&CurrentUser>;
183 fn profile(&self) -> Result<&CurrentUser>;
185 fn require_permission(&self, slug: &str) -> Result<&CurrentUser>;
186 fn require_role(&self, slug: &str) -> Result<&CurrentUser>;
187 fn password_confirmed(&self) -> bool;
188
189 fn can<P: crate::Policy<R> + Default, R>(&self, ability: crate::Ability, resource: &R) -> bool;
191
192 fn authorize<P: crate::Policy<R> + Default, R>(
194 &self,
195 ability: crate::Ability,
196 resource: &R,
197 ) -> Result<&CurrentUser>;
198
199 fn authorize_with(
201 &self,
202 f: impl FnOnce(&CurrentUser) -> bool,
203 ) -> Result<&CurrentUser>;
204
205 fn login_user(&mut self, user: CurrentUser);
212
213 fn logout_user(&mut self);
215
216 fn logout_other_sessions(
218 &self,
219 ) -> impl std::future::Future<Output = Result<u64>> + Send;
220
221 fn logout_all_sessions(
223 &mut self,
224 ) -> impl std::future::Future<Output = Result<u64>> + Send;
225}
226
227impl AuthExt for Request {
228 fn current_user(&self) -> Option<&CurrentUser> {
229 self.get::<CurrentUser>()
230 }
231
232 fn require_current_user(&self) -> Result<&CurrentUser> {
233 self.get::<CurrentUser>().ok_or(Error::Unauthorized)
234 }
235
236 fn profile(&self) -> Result<&CurrentUser> {
237 self.require_current_user()
238 }
239
240 fn require_permission(&self, slug: &str) -> Result<&CurrentUser> {
241 let u = self.require_current_user()?;
242 if u.has_permission(slug) {
243 Ok(u)
244 } else {
245 Err(Error::Forbidden)
246 }
247 }
248
249 fn require_role(&self, slug: &str) -> Result<&CurrentUser> {
250 let u = self.require_current_user()?;
251 if u.has_role(slug) {
252 Ok(u)
253 } else {
254 Err(Error::Forbidden)
255 }
256 }
257
258 fn password_confirmed(&self) -> bool {
259 is_password_confirmed(self)
260 }
261
262 fn can<P: crate::Policy<R> + Default, R>(&self, ability: crate::Ability, resource: &R) -> bool {
263 match self.current_user() {
264 Some(u) => crate::policy::can_ability::<P, R>(u, ability, resource),
265 None => false,
266 }
267 }
268
269 fn authorize<P: crate::Policy<R> + Default, R>(
270 &self,
271 ability: crate::Ability,
272 resource: &R,
273 ) -> Result<&CurrentUser> {
274 let u = self.require_current_user()?;
275 crate::policy::authorize_ability::<P, R>(u, ability, resource)?;
276 Ok(u)
277 }
278
279 fn authorize_with(&self, f: impl FnOnce(&CurrentUser) -> bool) -> Result<&CurrentUser> {
280 let u = self.require_current_user()?;
281 if f(u) {
282 Ok(u)
283 } else {
284 Err(Error::Forbidden)
285 }
286 }
287
288 fn login_user(&mut self, user: CurrentUser) {
289 self.session().remove(PENDING_2FA_KEY);
290 self.session().remove(PASSWORD_CONFIRMED_AT);
291 let id = user.id.to_string();
292 self.login(id, user);
293 }
294
295 fn logout_user(&mut self) {
296 self.session().remove(PENDING_2FA_KEY);
297 self.session().remove(PASSWORD_CONFIRMED_AT);
298 let _ = self.take::<CurrentUser>();
299 self.logout();
300 }
301
302 async fn logout_other_sessions(&self) -> Result<u64> {
303 SessionExt::logout_other_sessions(self).await
304 }
305
306 async fn logout_all_sessions(&mut self) -> Result<u64> {
307 self.session().remove(PENDING_2FA_KEY);
308 self.session().remove(PASSWORD_CONFIRMED_AT);
309 let n = SessionExt::logout_all_sessions(self).await?;
310 let _ = self.take::<CurrentUser>();
311 let _ = self.take::<sova_passport::Authenticated>();
312 Ok(n)
313 }
314}