1use sofie::db::{Outcome, Plan, Reply, Resolve, Stamp};
2use sofie::http::{PrincipalRecord, RequestContext, SignInRight};
3use sofie::prelude::*;
4use sofie::serde_json::json;
5
6use crate::{AccountId, MemberArea};
7
8struct Identity;
9
10impl sofie::http::Admitter for Identity {}
11
12const RIGHT: SignInRight = SignInRight::of::<Identity>();
13#[cfg(any(feature = "passkeys", feature = "oauth", feature = "totp"))]
14const SIGN_IN_AGAIN: &str = "Sign in again first: this needs a sign-in from the last ten minutes.";
15#[cfg(feature = "email-verification")]
16const UNCONFIRMED: &str = "Confirm your email address first: follow the link mailed to it.";
17
18pub struct SignedIn {
19 #[cfg(feature = "impersonation")]
20 session: sofie::db::Uuid,
21 #[cfg(feature = "sessions")]
22 record: crate::sessions::AccountSession,
23}
24
25pub(crate) fn sign_in<A: Area>(
27 session: &Session,
28 account: AccountId,
29 clock: &Clock,
30) -> Result<SignedIn> {
31 sign_in_for::<A>(
32 session,
33 RIGHT,
34 Id::from_uuid(account.uuid()),
35 account,
36 clock,
37 )
38}
39
40pub fn sign_in_for<A: Area>(
44 session: &Session,
45 right: SignInRight,
46 principal: Id<PrincipalRecord<A>>,
47 account: AccountId,
48 clock: &Clock,
49) -> Result<SignedIn> {
50 let id = session.sign_in::<A>(right, principal)?;
51 #[cfg(not(feature = "sessions"))]
52 let _ = (id, account, clock);
53 Ok(SignedIn {
54 #[cfg(feature = "impersonation")]
55 session: id,
56 #[cfg(feature = "sessions")]
57 record: crate::sessions::started(
58 id,
59 account,
60 A::NAME,
61 principal.uuid(),
62 session.user_agent().as_deref(),
63 clock.now(),
64 )?,
65 })
66}
67
68#[cfg(feature = "impersonation")]
70pub(crate) fn resume<A: Area>(session: &Session, resumed: &crate::sessions::Resumable) {
71 session.resume::<A>(
72 RIGHT,
73 resumed.session,
74 Id::from_uuid(resumed.principal),
75 resumed.signed_in_at.timestamp(),
76 );
77}
78
79#[cfg(feature = "impersonation")]
80impl SignedIn {
81 pub(crate) fn session(&self) -> sofie::db::Uuid {
82 self.session
83 }
84
85 pub(crate) fn impersonated(mut self, by: &Email, until: sofie::core::Instant) -> Self {
86 self.record.impersonated(by, until);
87 self
88 }
89}
90
91impl Outcome for SignedIn {
92 #[cfg(feature = "sessions")]
93 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
94 self.record.plan(plan)
95 }
96
97 #[cfg(feature = "sessions")]
98 fn committed(&mut self, stamp: Stamp) {
99 self.record.committed(stamp);
100 }
101}
102
103#[cfg(any(
105 feature = "passkeys",
106 feature = "oauth",
107 feature = "totp",
108 feature = "account-data",
109 feature = "session-screens",
110 feature = "token-screens"
111))]
112pub(crate) struct OwnSession;
113
114#[cfg(any(
115 feature = "passkeys",
116 feature = "oauth",
117 feature = "totp",
118 feature = "account-data",
119 feature = "session-screens",
120 feature = "token-screens"
121))]
122impl<A: Area, M> Resolve<RequestContext<A, M>> for OwnSession {
123 #[cfg_attr(
124 not(feature = "impersonation"),
125 expect(clippy::unused_async_trait_impl)
126 )]
127 async fn resolve(context: &mut RequestContext<A, M>) -> Result<Self> {
128 #[cfg(feature = "impersonation")]
129 if crate::impersonation::Impersonating::resolve(context)
130 .await?
131 .by()
132 .is_some()
133 {
134 return Err(Error::Rule(
135 "An impersonation cannot change how this account signs in, end its sessions, export or close it."
136 .to_owned(),
137 ));
138 }
139 #[cfg(not(feature = "impersonation"))]
140 let _ = context;
141 Ok(Self)
142 }
143}
144
145#[cfg(any(feature = "passkeys", feature = "oauth", feature = "totp"))]
147pub(crate) struct RecentSignIn;
148
149#[cfg(any(feature = "passkeys", feature = "oauth", feature = "totp"))]
150impl<A: Area> Resolve<RequestContext<A>> for RecentSignIn {
151 async fn resolve(context: &mut RequestContext<A>) -> Result<Self> {
152 if !SignedInRecently::resolve(context).await?.0 {
153 return Err(Error::Rule(SIGN_IN_AGAIN.to_owned()));
154 }
155 Ok(Self)
156 }
157}
158
159#[cfg(any(feature = "passkeys", feature = "oauth", feature = "totp"))]
161pub(crate) struct SignedInRecently(pub(crate) bool);
162
163#[cfg(any(feature = "passkeys", feature = "oauth", feature = "totp"))]
164impl<A: Area, M> Resolve<RequestContext<A, M>> for SignedInRecently {
165 async fn resolve(context: &mut RequestContext<A, M>) -> Result<Self> {
166 OwnSession::resolve(context).await?;
167 let current = sofie::http::CurrentSession::resolve(context).await?.id();
168 let now = Clock::resolve(context).await?.now();
169 let read = Read::resolve(context).await?;
170 let Some(current) = current else {
171 return Ok(Self(false));
172 };
173 crate::sessions::signed_in_recently(&read, current, now)
174 .await
175 .map(Self)
176 }
177}
178
179pub(crate) struct Door {
180 session: Session,
181 read: Read,
182 clock: Clock,
183 #[cfg(feature = "totp")]
184 urls: sofie::http::SignedUrls,
185 #[cfg(feature = "totp")]
186 next: sofie::http::Next,
187}
188
189impl Resolve<RequestContext<Web>> for Door {
190 async fn resolve(context: &mut RequestContext<Web>) -> Result<Self> {
191 Ok(Self {
192 session: Session::resolve(context).await?,
193 read: Read::resolve(context).await?,
194 clock: Clock::resolve(context).await?,
195 #[cfg(feature = "totp")]
196 urls: sofie::http::SignedUrls::resolve(context).await?,
197 #[cfg(feature = "totp")]
198 next: sofie::http::Next::resolve(context).await?,
199 })
200 }
201}
202
203pub(crate) struct Entered<T = ()> {
206 signed_in: Option<SignedIn>,
207 #[cfg(feature = "totp")]
208 pending: Option<crate::account_link::AccountLink>,
209 location: Option<String>,
210 saved: T,
211}
212
213impl Door {
214 pub(crate) fn read(&self) -> &Read {
215 &self.read
216 }
217
218 #[cfg(any(feature = "links", feature = "passkeys", feature = "oauth"))]
219 pub(crate) fn clock(&self) -> &Clock {
220 &self.clock
221 }
222
223 #[cfg(any(feature = "passkeys", feature = "oauth"))]
224 pub(crate) fn session(&self) -> &Session {
225 &self.session
226 }
227
228 pub(crate) async fn enter<A: MemberArea>(&self, account: AccountId) -> Result<Entered> {
229 self.enter_after::<A>(account, false).await
230 }
231
232 pub(crate) async fn enter_after<A: MemberArea>(
235 &self,
236 account: AccountId,
237 second_proved: bool,
238 ) -> Result<Entered> {
239 #[cfg(feature = "email-verification")]
240 if A::VERIFIED_EMAIL
241 && crate::account::Account::stored(&self.read, account)
242 .await?
243 .verified_at()
244 .is_none()
245 {
246 return Err(Error::Rule(UNCONFIRMED.to_owned()));
247 }
248 self.admit::<A>(account, second_proved).await
249 }
250
251 #[cfg(any(feature = "links", feature = "oauth"))]
254 pub(crate) async fn enter_confirming<A: MemberArea>(
255 &self,
256 account: AccountId,
257 distrusted: bool,
258 ) -> Result<Entered> {
259 self.admit::<A>(account, distrusted).await
260 }
261
262 #[cfg_attr(not(feature = "totp"), expect(clippy::unused_async_trait_impl))]
263 async fn admit<A: MemberArea>(
264 &self,
265 account: AccountId,
266 second_proved: bool,
267 ) -> Result<Entered> {
268 #[cfg(not(feature = "totp"))]
269 let _ = second_proved;
270 #[cfg(feature = "totp")]
271 if !second_proved
272 && crate::two_factor::required(&self.read, account)
273 .await?
274 .is_some()
275 {
276 let (pending, location) =
277 crate::two_factor::asking::<A>(account, &self.urls, &self.clock, &self.next)?;
278 return Ok(Entered {
279 signed_in: None,
280 pending: Some(pending),
281 location: Some(location),
282 saved: (),
283 });
284 }
285 Ok(Entered {
286 signed_in: Some(sign_in::<A>(&self.session, account, &self.clock)?),
287 #[cfg(feature = "totp")]
288 pending: None,
289 location: None,
290 saved: (),
291 })
292 }
293}
294
295impl Entered {
296 #[cfg(feature = "passkeys")]
298 pub(crate) fn located<A: MemberArea>(self) -> Self {
299 Self {
300 location: self.location.or_else(|| Some(A::HOME.to_owned())),
301 ..self
302 }
303 }
304
305 pub(crate) fn saving<T: Outcome>(self, saved: T) -> Entered<T> {
306 Entered {
307 signed_in: self.signed_in,
308 #[cfg(feature = "totp")]
309 pending: self.pending,
310 location: self.location,
311 saved,
312 }
313 }
314}
315
316impl<T: Outcome> Outcome for Entered<T> {
317 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
318 self.saved.plan(plan)?;
319 if let Some(signed_in) = &self.signed_in {
320 signed_in.plan(plan)?;
321 }
322 #[cfg(feature = "totp")]
323 if let Some(pending) = &self.pending {
324 pending.plan(plan)?;
325 }
326 Ok(())
327 }
328
329 fn committed(&mut self, stamp: Stamp) {
330 self.saved.committed(stamp);
331 if let Some(signed_in) = &mut self.signed_in {
332 signed_in.committed(stamp);
333 }
334 #[cfg(feature = "totp")]
335 if let Some(pending) = &mut self.pending {
336 pending.committed(stamp);
337 }
338 }
339
340 fn reply(self) -> Result<Reply> {
341 match self.location {
342 Some(location) => Ok(Reply::Json(json!({
343 "location": location,
344 "second_factor": self.signed_in.is_none(),
345 }))),
346 None => Ok(Reply::Empty),
347 }
348 }
349}