1use std::collections::BTreeMap;
4
5use crate::DbBackendConfig;
6use crate::operations::{
7 AppSchema, DatabaseError, DatabaseOperations, Mutation, Predicate, Query, Transaction,
8 WriteValue,
9};
10use serde_json::Value;
11
12pub fn account_profile_view(
15 adapter: &dyn DatabaseOperations,
16 config: &DbBackendConfig,
17 schema: &AppSchema,
18 actor_id: i64,
19) -> Result<Value, DatabaseError> {
20 if actor_id <= 0 {
21 return Err(DatabaseError::AccessDenied);
22 }
23 let mut accounts = adapter.query(
24 config,
25 schema,
26 &Query {
27 entity: "user_account".to_owned(),
28 fields: ["id", "username", "email", "display_name", "is_active"]
29 .into_iter()
30 .map(ToOwned::to_owned)
31 .collect(),
32 filter: Some(Predicate::All(vec![
33 Predicate::Equal("id".to_owned(), Value::from(actor_id)),
34 Predicate::Equal("is_active".to_owned(), Value::Bool(true)),
35 ])),
36 order: Vec::new(),
37 limit: 1,
38 offset: 0,
39 },
40 )?;
41 let mut account = accounts.pop().ok_or(DatabaseError::MissingRecord)?;
42 let profiles = adapter.query(
43 config,
44 schema,
45 &Query {
46 entity: "user_profile".to_owned(),
47 fields: schema
48 .entities
49 .iter()
50 .find(|entity| entity.name == "user_profile")
51 .ok_or(DatabaseError::InvalidContract)?
52 .fields
53 .iter()
54 .map(|field| field.name.clone())
55 .collect(),
56 filter: Some(Predicate::Equal(
57 "user_id".to_owned(),
58 Value::from(actor_id),
59 )),
60 order: Vec::new(),
61 limit: 2,
62 offset: 0,
63 },
64 )?;
65 if profiles.len() > 1 {
66 return Err(DatabaseError::Conflict);
67 }
68 let profile = profiles
69 .into_iter()
70 .next()
71 .ok_or(DatabaseError::MissingRecord)?;
72 account.extend(
73 profile
74 .into_iter()
75 .filter(|(name, _)| !matches!(name.as_str(), "id" | "user_id" | "created_at")),
76 );
77 let subscriptions = adapter.query(
78 config,
79 schema,
80 &Query {
81 entity: "subscription".to_owned(),
82 fields: vec![
83 "id".to_owned(),
84 "user_id".to_owned(),
85 "business_id".to_owned(),
86 ],
87 filter: Some(Predicate::Equal(
88 "user_id".to_owned(),
89 Value::from(actor_id),
90 )),
91 order: Vec::new(),
92 limit: 257,
93 offset: 0,
94 },
95 )?;
96 if subscriptions.len() > 256 {
97 return Err(DatabaseError::CapacityExceeded);
98 }
99 let ids = subscriptions
100 .iter()
101 .map(|row| {
102 row.get("business_id")
103 .cloned()
104 .ok_or(DatabaseError::StorageFailure)
105 })
106 .collect::<Result<Vec<_>, _>>()?;
107 let businesses = crate::operations::query_related_rows(
108 adapter,
109 config,
110 schema,
111 crate::operations::RelatedRowsQuery {
112 entity: "business",
113 foreign_key: "id",
114 keys: &ids,
115 fields: &["id".to_owned(), "name".to_owned(), "slug".to_owned()],
116 maximum: 256,
117 },
118 )?;
119 if businesses.len() > 256 {
120 return Err(DatabaseError::CapacityExceeded);
121 }
122 let mut calendars = adapter.query(
123 config,
124 schema,
125 &Query {
126 entity: "google_calendar_connection".to_owned(),
127 fields: vec!["user_id".to_owned(), "status".to_owned(), "mode".to_owned()],
128 filter: Some(Predicate::All(vec![
129 Predicate::Equal("user_id".to_owned(), Value::from(actor_id)),
130 Predicate::Equal("mode".to_owned(), Value::String("user".to_owned())),
131 ])),
132 order: Vec::new(),
133 limit: 2,
134 offset: 0,
135 },
136 )?;
137 if calendars.len() > 1 {
138 return Err(DatabaseError::Conflict);
139 }
140 for calendar in &mut calendars {
141 let status = calendar
142 .get("status")
143 .cloned()
144 .ok_or(DatabaseError::StorageFailure)?;
145 calendar.insert("connection_status".to_owned(), status);
146 }
147 Ok(
148 serde_json::json!({"users": [account], "subscriptions": subscriptions, "businesses": businesses, "google_calendar_connections": calendars}),
149 )
150}
151
152pub fn update_account_profile(
155 adapter: &dyn DatabaseOperations,
156 config: &DbBackendConfig,
157 schema: &AppSchema,
158 actor_id: i64,
159 input: &serde_json::Map<String, Value>,
160) -> Result<(), DatabaseError> {
161 if actor_id <= 0
162 || input.is_empty()
163 || input.keys().any(|name| {
164 !matches!(
165 name.as_str(),
166 "full_name" | "phone_number" | "age" | "address" | "payment_preference"
167 )
168 })
169 || input
170 .get("full_name")
171 .and_then(Value::as_str)
172 .is_none_or(|value| value.trim().is_empty() || value.len() > 256)
173 {
174 return Err(DatabaseError::InvalidInput);
175 }
176 if input
177 .get("phone_number")
178 .is_some_and(|value| value.as_str().is_none_or(|value| value.len() > 64))
179 || input
180 .get("address")
181 .is_some_and(|value| value.as_str().is_none_or(|value| value.len() > 2048))
182 || input.get("age").is_some_and(|value| {
183 !value.is_null() && value.as_i64().is_none_or(|age| !(0..=150).contains(&age))
184 })
185 || input.get("payment_preference").is_some_and(|value| {
186 value
187 .as_str()
188 .is_none_or(|value| !matches!(value, "pay_on_site" | "credit_card"))
189 })
190 {
191 return Err(DatabaseError::InvalidInput);
192 }
193 let ownership = Predicate::Equal("user_id".to_owned(), Value::from(actor_id));
194 let rows = adapter.query(
195 config,
196 schema,
197 &Query {
198 entity: "user_profile".to_owned(),
199 fields: input.keys().cloned().collect(),
200 filter: Some(ownership.clone()),
201 order: Vec::new(),
202 limit: 2,
203 offset: 0,
204 },
205 )?;
206 if rows.len() > 1 {
207 return Err(DatabaseError::Conflict);
208 }
209 let current = rows.first().ok_or(DatabaseError::MissingRecord)?;
210 let mut guards = vec![ownership];
211 let mut values = BTreeMap::new();
212 for (field, value) in input {
213 let previous = current.get(field).ok_or(DatabaseError::StorageFailure)?;
214 if previous == value {
215 continue;
216 }
217 guards.push(if previous.is_null() {
218 Predicate::IsNull(field.clone())
219 } else {
220 Predicate::Equal(field.clone(), previous.clone())
221 });
222 values.insert(field.clone(), WriteValue::Literal(value.clone()));
223 }
224 if values.is_empty() {
225 return Ok(());
226 }
227 adapter.transact(
228 config,
229 schema,
230 &Transaction {
231 steps: vec![
232 Mutation::RequireCount {
233 entity: "user_account".to_owned(),
234 filter: Predicate::All(vec![
235 Predicate::Equal("id".to_owned(), Value::from(actor_id)),
236 Predicate::Equal("is_active".to_owned(), Value::Bool(true)),
237 ]),
238 minimum: 1,
239 maximum: Some(1),
240 },
241 Mutation::Update {
242 entity: "user_profile".to_owned(),
243 filter: Predicate::All(guards),
244 values,
245 expected_rows: Some(1),
246 },
247 ],
248 idempotency_key: None,
249 },
250 )?;
251 Ok(())
252}
253
254pub fn register_account(
256 adapter: &dyn DatabaseOperations,
257 config: &DbBackendConfig,
258 schema: &AppSchema,
259 username: &str,
260 email: &str,
261 password_hash: &str,
262) -> Result<i64, DatabaseError> {
263 if username.is_empty()
264 || username.len() > 128
265 || !username
266 .bytes()
267 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
268 || email.is_empty()
269 || email.len() > 254
270 || email.split_once('@').is_none_or(|(local, domain)| {
271 local.is_empty() || domain.is_empty() || !domain.contains('.')
272 })
273 || password_hash.is_empty()
274 || password_hash.len() > 1024
275 {
276 return Err(DatabaseError::InvalidInput);
277 }
278 let result = adapter.transact(
279 config,
280 schema,
281 &Transaction {
282 steps: vec![
283 Mutation::Insert {
284 entity: "user_account".to_owned(),
285 values: BTreeMap::from([
286 (
287 "username".to_owned(),
288 WriteValue::Literal(Value::String(username.to_owned())),
289 ),
290 (
291 "email".to_owned(),
292 WriteValue::Literal(Value::String(email.to_owned())),
293 ),
294 (
295 "password_hash".to_owned(),
296 WriteValue::Literal(Value::String(password_hash.to_owned())),
297 ),
298 (
299 "is_active".to_owned(),
300 WriteValue::Literal(Value::Bool(true)),
301 ),
302 ]),
303 },
304 Mutation::Insert {
305 entity: "user_profile".to_owned(),
306 values: BTreeMap::from([
307 ("user_id".to_owned(), WriteValue::GeneratedId(0)),
308 (
309 "account_type".to_owned(),
310 WriteValue::Literal(Value::String("business_customer".to_owned())),
311 ),
312 (
313 "payment_preference".to_owned(),
314 WriteValue::Literal(Value::String("pay_on_site".to_owned())),
315 ),
316 (
317 "email_verified".to_owned(),
318 WriteValue::Literal(Value::Bool(false)),
319 ),
320 ]),
321 },
322 ],
323 idempotency_key: None,
324 },
325 )?;
326 result
327 .generated_ids
328 .first()
329 .copied()
330 .flatten()
331 .filter(|id| *id > 0)
332 .ok_or(DatabaseError::StorageFailure)
333}
334
335pub fn issue_password_reset(
337 adapter: &dyn DatabaseOperations,
338 config: &DbBackendConfig,
339 schema: &AppSchema,
340 email: &str,
341 token_hash: &str,
342 delivery_secret: &str,
343 expires_at: &str,
344) -> Result<bool, DatabaseError> {
345 if email.len() > 320
346 || email.split_once('@').is_none_or(|(local, domain)| {
347 local.is_empty() || domain.is_empty() || !domain.contains('.')
348 })
349 || token_hash.len() != 64
350 || !token_hash.bytes().all(|byte| byte.is_ascii_hexdigit())
351 || delivery_secret.len() != 64
352 || !delivery_secret.bytes().all(|byte| byte.is_ascii_hexdigit())
353 || expires_at.is_empty()
354 || expires_at.len() > 64
355 {
356 return Err(DatabaseError::InvalidInput);
357 }
358 let users = adapter.query(
359 config,
360 schema,
361 &Query {
362 entity: "user_account".to_owned(),
363 fields: vec!["id".to_owned()],
364 filter: Some(Predicate::Equal(
365 "email".to_owned(),
366 Value::String(email.to_owned()),
367 )),
368 order: Vec::new(),
369 limit: 2,
370 offset: 0,
371 },
372 )?;
373 if users.len() > 1 {
374 return Err(DatabaseError::Conflict);
375 }
376 let Some(user) = users.first() else {
377 return Ok(false);
378 };
379 let user_id = user
380 .get("id")
381 .and_then(Value::as_i64)
382 .filter(|id| *id > 0)
383 .ok_or(DatabaseError::StorageFailure)?;
384 adapter.transact(
385 config,
386 schema,
387 &Transaction {
388 steps: vec![
389 Mutation::RequireCount {
390 entity: "user_account".to_owned(),
391 filter: Predicate::All(vec![
392 Predicate::Equal("id".to_owned(), Value::from(user_id)),
393 Predicate::Equal("email".to_owned(), Value::String(email.to_owned())),
394 ]),
395 minimum: 1,
396 maximum: Some(1),
397 },
398 Mutation::Insert {
399 entity: "password_reset".to_owned(),
400 values: BTreeMap::from([
401 (
402 "user_id".to_owned(),
403 WriteValue::Literal(Value::from(user_id)),
404 ),
405 (
406 "token_hash".to_owned(),
407 WriteValue::Literal(Value::String(token_hash.to_owned())),
408 ),
409 (
410 "expires_at".to_owned(),
411 WriteValue::Literal(Value::String(expires_at.to_owned())),
412 ),
413 ]),
414 },
415 Mutation::Insert {
416 entity: "notification_delivery".to_owned(),
417 values: BTreeMap::from([
418 (
419 "adapter_id".to_owned(),
420 WriteValue::Literal(Value::String("local_reset_queue".to_owned())),
421 ),
422 (
423 "channel".to_owned(),
424 WriteValue::Literal(Value::String("email".to_owned())),
425 ),
426 (
427 "recipient".to_owned(),
428 WriteValue::Literal(Value::String(email.to_owned())),
429 ),
430 (
431 "template_id".to_owned(),
432 WriteValue::Literal(Value::String("password_reset".to_owned())),
433 ),
434 (
435 "delivery_status".to_owned(),
436 WriteValue::Literal(Value::String("queued".to_owned())),
437 ),
438 (
439 "resource_id".to_owned(),
440 WriteValue::Literal(Value::String(delivery_secret.to_owned())),
441 ),
442 (
443 "max_attempts".to_owned(),
444 WriteValue::Literal(Value::from(0)),
445 ),
446 (
447 "backoff_seconds".to_owned(),
448 WriteValue::Literal(Value::from(0)),
449 ),
450 ]),
451 },
452 ],
453 idempotency_key: None,
454 },
455 )?;
456 Ok(true)
457}
458
459pub fn complete_password_reset(
461 adapter: &dyn DatabaseOperations,
462 config: &DbBackendConfig,
463 schema: &AppSchema,
464 token_hash: &str,
465 password_hash: &str,
466 now: &str,
467) -> Result<Option<i64>, DatabaseError> {
468 if token_hash.len() != 64
469 || !token_hash.bytes().all(|byte| byte.is_ascii_hexdigit())
470 || password_hash.is_empty()
471 || password_hash.len() > 1024
472 || now.is_empty()
473 || now.len() > 64
474 {
475 return Err(DatabaseError::InvalidInput);
476 }
477 let live_token = Predicate::All(vec![
478 Predicate::Equal(
479 "token_hash".to_owned(),
480 Value::String(token_hash.to_owned()),
481 ),
482 Predicate::IsNull("claimed_at".to_owned()),
483 Predicate::GreaterThan("expires_at".to_owned(), Value::String(now.to_owned())),
484 ]);
485 let resets = adapter.query(
486 config,
487 schema,
488 &Query {
489 entity: "password_reset".to_owned(),
490 fields: vec!["id".to_owned(), "user_id".to_owned()],
491 filter: Some(live_token.clone()),
492 order: Vec::new(),
493 limit: 2,
494 offset: 0,
495 },
496 )?;
497 if resets.len() > 1 {
498 return Err(DatabaseError::Conflict);
499 }
500 let Some(reset) = resets.first() else {
501 return Ok(None);
502 };
503 let reset_id = reset
504 .get("id")
505 .and_then(Value::as_i64)
506 .filter(|id| *id > 0)
507 .ok_or(DatabaseError::StorageFailure)?;
508 let user_id = reset
509 .get("user_id")
510 .and_then(Value::as_i64)
511 .filter(|id| *id > 0)
512 .ok_or(DatabaseError::StorageFailure)?;
513 let reset_filter = Predicate::All(vec![
514 live_token,
515 Predicate::Equal("id".to_owned(), Value::from(reset_id)),
516 ]);
517 let transaction = Transaction {
518 steps: vec![
519 Mutation::RequireCount {
520 entity: "password_reset".to_owned(),
521 filter: reset_filter.clone(),
522 minimum: 1,
523 maximum: Some(1),
524 },
525 Mutation::Update {
526 entity: "password_reset".to_owned(),
527 filter: reset_filter,
528 values: BTreeMap::from([(
529 "claimed_at".to_owned(),
530 WriteValue::Literal(Value::String(now.to_owned())),
531 )]),
532 expected_rows: Some(1),
533 },
534 Mutation::Update {
535 entity: "user_account".to_owned(),
536 filter: Predicate::Equal("id".to_owned(), Value::from(user_id)),
537 values: BTreeMap::from([(
538 "password_hash".to_owned(),
539 WriteValue::Literal(Value::String(password_hash.to_owned())),
540 )]),
541 expected_rows: Some(1),
542 },
543 Mutation::Update {
544 entity: "auth_session".to_owned(),
545 filter: Predicate::All(vec![
546 Predicate::Equal("user_id".to_owned(), Value::from(user_id)),
547 Predicate::IsNull("revoked_at".to_owned()),
548 ]),
549 values: BTreeMap::from([(
550 "revoked_at".to_owned(),
551 WriteValue::Literal(Value::String(now.to_owned())),
552 )]),
553 expected_rows: None,
554 },
555 ],
556 idempotency_key: None,
557 };
558 adapter.transact(config, schema, &transaction)?;
559 Ok(Some(user_id))
560}
561
562pub fn complete_email_verification(
564 adapter: &dyn DatabaseOperations,
565 config: &DbBackendConfig,
566 schema: &AppSchema,
567 token_hash: &str,
568 now: &str,
569) -> Result<Option<i64>, DatabaseError> {
570 if token_hash.is_empty()
571 || token_hash.len() > 256
572 || token_hash.contains(['\0', '\r', '\n'])
573 || now.is_empty()
574 || now.len() > 64
575 {
576 return Err(DatabaseError::InvalidInput);
577 }
578 let live_token = Predicate::All(vec![
579 Predicate::Equal(
580 "token_hash".to_owned(),
581 Value::String(token_hash.to_owned()),
582 ),
583 Predicate::IsNull("claimed_at".to_owned()),
584 Predicate::GreaterThan("expires_at".to_owned(), Value::String(now.to_owned())),
585 ]);
586 let verifications = adapter.query(
587 config,
588 schema,
589 &Query {
590 entity: "email_verification".to_owned(),
591 fields: vec!["id".to_owned(), "user_id".to_owned()],
592 filter: Some(live_token.clone()),
593 order: Vec::new(),
594 limit: 2,
595 offset: 0,
596 },
597 )?;
598 if verifications.len() > 1 {
599 return Err(DatabaseError::Conflict);
600 }
601 let Some(verification) = verifications.first() else {
602 return Ok(None);
603 };
604 let verification_id = verification
605 .get("id")
606 .and_then(Value::as_i64)
607 .filter(|id| *id > 0)
608 .ok_or(DatabaseError::StorageFailure)?;
609 let user_id = verification
610 .get("user_id")
611 .and_then(Value::as_i64)
612 .filter(|id| *id > 0)
613 .ok_or(DatabaseError::StorageFailure)?;
614 let filter = Predicate::All(vec![
615 live_token,
616 Predicate::Equal("id".to_owned(), Value::from(verification_id)),
617 ]);
618 adapter.transact(
619 config,
620 schema,
621 &Transaction {
622 steps: vec![
623 Mutation::RequireCount {
624 entity: "email_verification".to_owned(),
625 filter: filter.clone(),
626 minimum: 1,
627 maximum: Some(1),
628 },
629 Mutation::Update {
630 entity: "email_verification".to_owned(),
631 filter,
632 values: BTreeMap::from([(
633 "claimed_at".to_owned(),
634 WriteValue::Literal(Value::String(now.to_owned())),
635 )]),
636 expected_rows: Some(1),
637 },
638 Mutation::Update {
639 entity: "user_profile".to_owned(),
640 filter: Predicate::Equal("user_id".to_owned(), Value::from(user_id)),
641 values: BTreeMap::from([(
642 "email_verified".to_owned(),
643 WriteValue::Literal(Value::Bool(true)),
644 )]),
645 expected_rows: Some(1),
646 },
647 ],
648 idempotency_key: None,
649 },
650 )?;
651 Ok(Some(user_id))
652}