1use coven_format::codes::{InviteCode, InviteKind, RestoreCode};
4use coven_foundation::id_source::{MemberId, StoreId};
5use coven_storage::{CloudProvider, InviteStorage, OAuthAccess, RestoreStorage, StorageConfig};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum CodeKind {
10 Restore,
13 Invite,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum CodeInfo {
21 Restore(CodeMetadata),
23 Invite(InviteInfo),
26}
27
28impl CodeInfo {
29 pub fn metadata(&self) -> &CodeMetadata {
31 match self {
32 Self::Restore(metadata) | Self::Invite(InviteInfo { metadata, .. }) => metadata,
33 }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct InviteInfo {
41 pub metadata: CodeMetadata,
43 pub store_name: String,
45 pub inviter: String,
49 pub kind: InviteKind,
52 pub expires_at: std::time::SystemTime,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct CodeMetadata {
60 pub store_id: StoreId,
62 pub member: MemberId,
65 pub cloud_provider: CloudProvider,
67 pub location: StorageConfig,
69 pub oauth_access: Option<OAuthAccess>,
71 pub carries_s3_key: bool,
75}
76
77#[derive(Clone, Debug, thiserror::Error)]
79pub enum CodeError {
80 #[error("invalid restore or invite code")]
82 Invalid,
83 #[error("expected {expected:?} code, received {actual:?}")]
85 WrongKind {
86 expected: CodeKind,
88 actual: CodeKind,
90 },
91 #[error("code is for store {actual}, not {expected}")]
95 WrongStore {
96 expected: StoreId,
98 actual: StoreId,
100 },
101 #[error("invite is for {expected:?}, but the join is for {actual:?}")]
104 WrongJoiner {
105 expected: InviteKind,
107 actual: InviteKind,
109 },
110}
111
112impl CodeError {
113 pub fn require_store(expected: StoreId, actual: StoreId) -> Result<(), Self> {
115 if actual == expected {
116 Ok(())
117 } else {
118 Err(Self::WrongStore { expected, actual })
119 }
120 }
121}
122
123pub fn decode_code_info(code: &str) -> Result<CodeInfo, CodeError> {
125 let (code, location, carries_s3_key) = decode(code)?;
126 let metadata = |store_id, member| CodeMetadata {
127 store_id,
128 member,
129 cloud_provider: location.provider(),
130 oauth_access: location.oauth_access(),
131 location,
132 carries_s3_key,
133 };
134 Ok(match code {
135 DecodedCode::Restore(code) => CodeInfo::Restore(metadata(code.store, code.member)),
136 DecodedCode::Invite(code) => CodeInfo::Invite(InviteInfo {
137 metadata: metadata(code.store, code.member),
138 store_name: code.name,
139 inviter: code.inviter,
140 kind: code.kind,
141 expires_at: std::time::UNIX_EPOCH + std::time::Duration::from_secs(code.expires_at),
142 }),
143 })
144}
145
146pub fn read_restore_code(code: &str) -> Result<RestoreCode, CodeError> {
149 match decode(code)?.0 {
150 DecodedCode::Restore(code) => Ok(code),
151 DecodedCode::Invite(_) => {
152 Err(CodeError::WrongKind { expected: CodeKind::Restore, actual: CodeKind::Invite })
153 }
154 }
155}
156
157pub fn read_invite_code(code: &str) -> Result<InviteCode, CodeError> {
159 match decode(code)?.0 {
160 DecodedCode::Invite(code) => Ok(code),
161 DecodedCode::Restore(_) => {
162 Err(CodeError::WrongKind { expected: CodeKind::Invite, actual: CodeKind::Restore })
163 }
164 }
165}
166
167enum DecodedCode {
168 Restore(RestoreCode),
169 Invite(InviteCode),
170}
171
172#[expect(
173 clippy::map_err_ignore,
174 reason = "a code that does not decode is Invalid, whichever field failed"
175)]
176fn decode(text: &str) -> Result<(DecodedCode, StorageConfig, bool), CodeError> {
177 if text.starts_with(RestoreCode::TEXT_PREFIX) {
178 let code = RestoreCode::from_text(text).map_err(|_| CodeError::Invalid)?;
179 let storage =
180 RestoreStorage::decode(code.storage.as_bytes()).map_err(|_| CodeError::Invalid)?;
181 let carries_s3_key = storage.s3_key.is_some();
182 Ok((DecodedCode::Restore(code), storage.location, carries_s3_key))
183 } else if text.starts_with(InviteCode::TEXT_PREFIX) {
184 let code = InviteCode::from_text(text).map_err(|_| CodeError::Invalid)?;
185 let (invitation, carries_s3_key) =
187 match InviteStorage::decode(code.storage.as_bytes()).map_err(|_| CodeError::Invalid)? {
188 InviteStorage::Account(invitation) => (invitation, false),
189 InviteStorage::S3 { invitation, .. } => (invitation, true),
190 };
191 Ok((DecodedCode::Invite(code), invitation.location().clone(), carries_s3_key))
192 } else {
193 Err(CodeError::Invalid)
194 }
195}
196
197#[cfg(test)]
198#[path = "codes_tests.rs"]
199mod tests;