netsblox_cloud_common/
lib.rs

1use mongodb::bson::{self, doc, document::Document, Bson, DateTime};
2pub use netsblox_api_common as api;
3use netsblox_api_common::{
4    oauth, ClientState, LibraryMetadata, NewUser, PublishState, RoleId, UserRole,
5};
6use netsblox_api_common::{
7    FriendInvite, FriendLinkState, GroupId, InvitationState, LinkedAccount, ProjectId, RoleData,
8    SaveState, ServiceHost, ServiceHostScope,
9};
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha512};
12use std::{
13    collections::HashMap,
14    time::{Duration, SystemTime},
15};
16use uuid::Uuid;
17
18#[derive(Serialize, Deserialize, Clone, Debug)]
19#[serde(rename_all = "camelCase")]
20pub struct User {
21    pub username: String,
22    pub email: String,
23    pub hash: String,
24    pub salt: Option<String>,
25    pub group_id: Option<GroupId>,
26    pub role: UserRole,
27    pub created_at: DateTime,
28    pub linked_accounts: Vec<LinkedAccount>,
29    pub services_hosts: Option<Vec<ServiceHost>>,
30    pub service_settings: HashMap<String, String>,
31}
32
33impl User {
34    pub fn is_member(&self) -> bool {
35        self.group_id.is_some()
36    }
37}
38
39impl From<User> for Bson {
40    fn from(user: User) -> Bson {
41        Bson::Document(doc! {
42            "username": user.username,
43            "email": user.email,
44            "hash": user.hash,
45            "salt": user.salt,
46            "groupId": user.group_id,
47            "role": user.role,
48            "createdAt": user.created_at,
49            "linkedAccounts": user.linked_accounts,
50            "servicesHosts": user.services_hosts,
51            "serviceSettings": bson::to_bson(&user.service_settings).unwrap(),
52        })
53    }
54}
55
56impl From<User> for netsblox_api_common::User {
57    fn from(user: User) -> netsblox_api_common::User {
58        netsblox_api_common::User {
59            username: user.username,
60            email: user.email,
61            group_id: user.group_id,
62            role: user.role,
63            created_at: user.created_at.to_system_time(),
64            linked_accounts: user.linked_accounts,
65            services_hosts: user.services_hosts,
66        }
67    }
68}
69
70impl From<NewUser> for User {
71    fn from(user_data: NewUser) -> Self {
72        let salt = passwords::PasswordGenerator::new()
73            .length(8)
74            .exclude_similar_characters(true)
75            .numbers(true)
76            .spaces(false)
77            .generate_one()
78            .unwrap_or_else(|_err| "salt".to_owned());
79
80        let hash: String = if let Some(pwd) = user_data.password {
81            sha512(&(pwd + &salt))
82        } else {
83            "None".to_owned()
84        };
85
86        User {
87            username: user_data.username,
88            hash,
89            salt: Some(salt),
90            email: user_data.email,
91            group_id: user_data.group_id,
92            created_at: DateTime::from_system_time(SystemTime::now()),
93            linked_accounts: std::vec::Vec::new(),
94            role: user_data.role.unwrap_or(UserRole::User),
95            services_hosts: None,
96            service_settings: HashMap::new(),
97        }
98    }
99}
100
101#[derive(Serialize, Deserialize, Clone)]
102#[serde(rename_all = "camelCase")]
103pub struct BannedAccount {
104    pub username: String,
105    pub email: String,
106    pub banned_at: DateTime,
107}
108
109impl BannedAccount {
110    pub fn new(username: String, email: String) -> BannedAccount {
111        let banned_at = DateTime::now();
112        BannedAccount {
113            username,
114            email,
115            banned_at,
116        }
117    }
118}
119
120impl From<BannedAccount> for Bson {
121    fn from(account: BannedAccount) -> Self {
122        Bson::Document(doc! {
123            "username": account.username,
124            "email": account.email,
125            "bannedAt": account.banned_at,
126        })
127    }
128}
129
130impl From<BannedAccount> for api::BannedAccount {
131    fn from(account: BannedAccount) -> Self {
132        api::BannedAccount {
133            username: account.username,
134            email: account.email,
135            banned_at: account.banned_at.into(),
136        }
137    }
138}
139
140#[derive(Serialize, Deserialize, Clone, Debug)]
141#[serde(rename_all = "camelCase")]
142pub struct Group {
143    pub id: GroupId,
144    pub owner: String,
145    pub name: String,
146    pub services_hosts: Option<Vec<ServiceHost>>,
147    pub service_settings: HashMap<String, String>,
148}
149
150impl Group {
151    pub fn new(owner: String, name: String) -> Self {
152        Self {
153            id: api::GroupId::new(Uuid::new_v4().to_string()),
154            name,
155            owner,
156            service_settings: HashMap::new(),
157            services_hosts: None,
158        }
159    }
160
161    pub fn from_data(owner: String, data: api::CreateGroupData) -> Self {
162        Self {
163            id: api::GroupId::new(Uuid::new_v4().to_string()),
164            owner,
165            name: data.name,
166            service_settings: HashMap::new(),
167            services_hosts: data.services_hosts,
168        }
169    }
170}
171
172impl From<Group> for netsblox_api_common::Group {
173    fn from(group: Group) -> netsblox_api_common::Group {
174        netsblox_api_common::Group {
175            id: group.id,
176            owner: group.owner,
177            name: group.name,
178            services_hosts: group.services_hosts,
179        }
180    }
181}
182
183impl From<Group> for Bson {
184    fn from(group: Group) -> Self {
185        let mut settings = Document::new();
186        group.service_settings.into_iter().for_each(|(k, v)| {
187            settings.insert(k, v);
188        });
189
190        Bson::Document(doc! {
191            "id": group.id,
192            "owner": group.owner,
193            "name": group.name,
194            "serviceSettings": settings,
195            "servicesHosts": group.services_hosts,
196        })
197    }
198}
199
200#[derive(Serialize, Deserialize, Clone, Debug)]
201#[serde(rename_all = "camelCase")]
202pub struct CollaborationInvite {
203    pub id: String,
204    pub sender: String,
205    pub receiver: String,
206    pub project_id: ProjectId,
207    pub state: InvitationState,
208    pub created_at: DateTime,
209}
210
211impl CollaborationInvite {
212    pub fn new(sender: String, receiver: String, project_id: ProjectId) -> Self {
213        CollaborationInvite {
214            id: Uuid::new_v4().to_string(),
215            sender,
216            receiver,
217            project_id,
218            state: InvitationState::Pending,
219            created_at: DateTime::from_system_time(SystemTime::now()),
220        }
221    }
222}
223
224impl From<CollaborationInvite> for Bson {
225    fn from(invite: CollaborationInvite) -> Self {
226        Bson::Document(doc! {
227            "id": invite.id,
228            "sender": invite.sender,
229            "receiver": invite.receiver,
230            "projectId": invite.project_id,
231            "state": invite.state,
232            "createdAt": invite.created_at,
233        })
234    }
235}
236
237impl From<CollaborationInvite> for netsblox_api_common::CollaborationInvite {
238    fn from(user: CollaborationInvite) -> netsblox_api_common::CollaborationInvite {
239        netsblox_api_common::CollaborationInvite {
240            id: user.id,
241            sender: user.sender,
242            receiver: user.receiver,
243            project_id: user.project_id,
244            state: user.state,
245            created_at: user.created_at.to_system_time(),
246        }
247    }
248}
249
250#[derive(Deserialize, Serialize, Clone, Debug)]
251#[serde(rename_all = "camelCase")]
252pub struct FriendLink {
253    pub id: api::FriendLinkId,
254    pub sender: String,
255    pub recipient: String,
256    pub state: FriendLinkState,
257    pub created_at: DateTime,
258    pub updated_at: DateTime,
259}
260
261impl FriendLink {
262    pub fn new(sender: String, recipient: String, state: Option<FriendLinkState>) -> FriendLink {
263        let created_at = DateTime::from_system_time(SystemTime::now());
264        FriendLink {
265            id: Uuid::new_v4().to_string(),
266            sender,
267            recipient,
268            state: state.unwrap_or(FriendLinkState::Pending),
269            created_at,
270            updated_at: created_at,
271        }
272    }
273}
274
275impl From<FriendLink> for api::FriendLink {
276    fn from(link: FriendLink) -> api::FriendLink {
277        api::FriendLink {
278            id: link.id,
279            sender: link.sender,
280            recipient: link.recipient,
281            state: link.state,
282            created_at: link.created_at.into(),
283            updated_at: link.updated_at.into(),
284        }
285    }
286}
287
288impl From<FriendLink> for FriendInvite {
289    fn from(link: FriendLink) -> FriendInvite {
290        FriendInvite {
291            id: link.id,
292            sender: link.sender,
293            recipient: link.recipient,
294            created_at: link.created_at.to_system_time(),
295        }
296    }
297}
298
299impl From<FriendLink> for Bson {
300    fn from(link: FriendLink) -> Bson {
301        Bson::Document(doc! {
302            "id": link.id,
303            "sender": link.sender,
304            "recipient": link.recipient,
305            "state": link.state,
306            "createdAt": link.created_at,
307            "updatedAt": link.updated_at,
308        })
309    }
310}
311
312#[derive(Deserialize, Serialize, Clone, Debug)]
313#[serde(rename_all = "camelCase")]
314pub struct NetworkTraceMetadata {
315    pub id: String,
316    pub start_time: DateTime,
317    pub end_time: Option<DateTime>,
318}
319
320impl NetworkTraceMetadata {
321    pub fn new() -> Self {
322        Self {
323            id: Uuid::new_v4().to_string(),
324            start_time: DateTime::now(),
325            end_time: None,
326        }
327    }
328}
329
330impl From<NetworkTraceMetadata> for Bson {
331    fn from(link: NetworkTraceMetadata) -> Bson {
332        Bson::Document(doc! {
333            "id": link.id,
334            "startTime": link.start_time,
335            "endTime": link.end_time,
336        })
337    }
338}
339
340impl From<NetworkTraceMetadata> for netsblox_api_common::NetworkTraceMetadata {
341    fn from(trace: NetworkTraceMetadata) -> netsblox_api_common::NetworkTraceMetadata {
342        netsblox_api_common::NetworkTraceMetadata {
343            id: trace.id,
344            start_time: trace.start_time.into(),
345            end_time: trace.end_time.map(|t| t.into()),
346        }
347    }
348}
349
350#[derive(Deserialize, Serialize, Clone, Debug)]
351#[serde(rename_all = "camelCase")]
352pub struct ProjectMetadata {
353    pub id: ProjectId,
354    pub owner: String,
355    pub name: String,
356    pub updated: DateTime,
357    pub state: PublishState,
358    pub collaborators: std::vec::Vec<String>,
359    pub origin_time: DateTime,
360    pub save_state: SaveState,
361    pub delete_at: Option<DateTime>,
362    pub network_traces: Vec<NetworkTraceMetadata>,
363    pub roles: HashMap<RoleId, RoleMetadata>,
364}
365
366impl ProjectMetadata {
367    pub fn new(
368        owner: &str,
369        name: &str,
370        roles: HashMap<RoleId, RoleMetadata>,
371        save_state: SaveState,
372    ) -> ProjectMetadata {
373        let origin_time = DateTime::now();
374
375        let delete_at = match save_state {
376            SaveState::Saved => None,
377            _ => {
378                // if not saved, set the project to be deleted in 10 minutes if not joined
379                let ten_minutes = Duration::new(10 * 60, 0);
380                let ten_mins_from_now = SystemTime::now().checked_add(ten_minutes).unwrap();
381                Some(DateTime::from_system_time(ten_mins_from_now))
382            }
383        };
384
385        ProjectMetadata {
386            id: ProjectId::new(Uuid::new_v4().to_string()),
387            owner: owner.to_owned(),
388            name: name.to_owned(),
389            updated: origin_time,
390            origin_time,
391            state: PublishState::Private,
392            collaborators: vec![],
393            save_state,
394            delete_at,
395            network_traces: Vec::new(),
396            roles,
397        }
398    }
399}
400
401impl From<ProjectMetadata> for Bson {
402    fn from(metadata: ProjectMetadata) -> Bson {
403        let mut roles = Document::new();
404        metadata.roles.into_iter().for_each(|(id, md)| {
405            roles.insert(id.as_str(), md);
406        });
407
408        Bson::Document(doc! {
409            "id": metadata.id,
410            "owner": metadata.owner,
411            "name": metadata.name,
412            "updated": metadata.updated,
413            "originTime": metadata.origin_time,
414            "state": metadata.state,
415            "collaborators": metadata.collaborators,
416            "saveState": metadata.save_state,
417            "roles": roles,
418            "deleteAt": metadata.delete_at,
419            "networkTraces": metadata.network_traces,
420        })
421    }
422}
423
424impl From<ProjectMetadata> for netsblox_api_common::ProjectMetadata {
425    fn from(metadata: ProjectMetadata) -> netsblox_api_common::ProjectMetadata {
426        netsblox_api_common::ProjectMetadata {
427            id: metadata.id,
428            owner: metadata.owner,
429            name: metadata.name,
430            origin_time: metadata.origin_time.to_system_time(),
431            updated: metadata.updated.to_system_time(),
432            state: metadata.state,
433            collaborators: metadata.collaborators,
434            save_state: metadata.save_state,
435            network_traces: metadata
436                .network_traces
437                .into_iter()
438                .map(|t| t.into())
439                .collect(),
440            roles: metadata
441                .roles
442                .into_iter()
443                .map(|(k, v)| (k, v.into()))
444                .collect(),
445        }
446    }
447}
448
449#[derive(Deserialize, Serialize, Clone, Debug)]
450#[serde(rename_all = "camelCase")]
451pub struct Project {
452    pub id: ProjectId,
453    pub owner: String,
454    pub name: String,
455    pub updated: DateTime,
456    pub state: PublishState,
457    pub collaborators: std::vec::Vec<String>,
458    pub origin_time: DateTime,
459    pub save_state: SaveState,
460    pub roles: HashMap<RoleId, RoleData>,
461}
462
463impl From<Project> for netsblox_api_common::Project {
464    fn from(project: Project) -> netsblox_api_common::Project {
465        netsblox_api_common::Project {
466            id: project.id,
467            owner: project.owner,
468            name: project.name,
469            origin_time: project.origin_time.to_system_time(),
470            updated: project.updated.to_system_time(),
471            state: project.state,
472            collaborators: project.collaborators,
473            save_state: project.save_state,
474            roles: project.roles,
475        }
476    }
477}
478
479#[derive(Deserialize, Serialize, Clone, Debug)]
480#[serde(rename_all = "camelCase")]
481pub struct RoleMetadata {
482    pub name: String,
483    pub code: String,
484    pub media: String,
485    pub updated: DateTime,
486}
487
488impl From<RoleMetadata> for netsblox_api_common::RoleMetadata {
489    fn from(metadata: RoleMetadata) -> netsblox_api_common::RoleMetadata {
490        netsblox_api_common::RoleMetadata {
491            name: metadata.name,
492            code: metadata.code,
493            media: metadata.media,
494        }
495    }
496}
497
498impl From<RoleMetadata> for Bson {
499    fn from(metadata: RoleMetadata) -> Bson {
500        Bson::Document(doc! {
501            "name": metadata.name,
502            "code": metadata.code,
503            "media": metadata.media,
504            "updated": metadata.updated,
505        })
506    }
507}
508
509#[derive(Deserialize, Serialize, Debug, Clone)]
510#[serde(rename_all = "camelCase")]
511pub struct OccupantInvite {
512    pub username: String,
513    pub project_id: ProjectId,
514    pub role_id: RoleId,
515    created_at: DateTime,
516}
517
518impl OccupantInvite {
519    pub fn new(target: String, project_id: ProjectId, role_id: RoleId) -> Self {
520        OccupantInvite {
521            project_id,
522            username: target,
523            role_id,
524            created_at: DateTime::from_system_time(SystemTime::now()),
525        }
526    }
527}
528
529impl From<OccupantInvite> for api::OccupantInvite {
530    fn from(invite: OccupantInvite) -> api::OccupantInvite {
531        api::OccupantInvite {
532            username: invite.username,
533            project_id: invite.project_id,
534            role_id: invite.role_id,
535            created_at: invite.created_at.into(),
536        }
537    }
538}
539
540#[derive(Deserialize, Serialize, Debug, Clone)]
541#[serde(rename_all = "camelCase")]
542pub struct SentMessage {
543    pub project_id: ProjectId,
544    pub recipients: Vec<ClientState>,
545    pub time: DateTime,
546    pub source: ClientState,
547
548    pub content: serde_json::Value,
549}
550
551/// log message type
552#[derive(Deserialize, Serialize, Debug, Clone)]
553#[serde(rename_all = "camelCase")]
554pub struct LogMessage {
555    pub sender: String,
556    pub recipients: Vec<String>,
557    pub content: serde_json::Value,
558    pub created_at: DateTime,
559}
560
561// NOTE: timestamped on conversion
562impl From<api::LogMessage> for LogMessage {
563    fn from(value: api::LogMessage) -> Self {
564        LogMessage {
565            sender: value.sender,
566            recipients: value.recipients,
567            content: value.content,
568            created_at: DateTime::now(),
569        }
570    }
571}
572
573impl From<LogMessage> for api::LogMessage {
574    fn from(value: LogMessage) -> Self {
575        api::LogMessage {
576            sender: value.sender,
577            recipients: value.recipients,
578            content: value.content,
579        }
580    }
581}
582
583impl SentMessage {
584    pub fn new(
585        project_id: ProjectId,
586        source: ClientState,
587        recipients: Vec<ClientState>,
588        content: serde_json::Value,
589    ) -> Self {
590        let time = DateTime::now();
591        SentMessage {
592            project_id,
593            recipients,
594            time,
595            source,
596            content,
597        }
598    }
599}
600
601impl From<SentMessage> for api::SentMessage {
602    fn from(msg: SentMessage) -> api::SentMessage {
603        api::SentMessage {
604            project_id: msg.project_id,
605            recipients: msg.recipients,
606            time: msg.time.into(),
607            source: msg.source,
608            content: msg.content,
609        }
610    }
611}
612
613#[derive(Deserialize, Serialize, Debug, Clone)]
614#[serde(rename_all = "camelCase")]
615pub struct SetPasswordToken {
616    pub username: String,
617    pub secret: String,
618    pub created_at: DateTime,
619}
620
621impl SetPasswordToken {
622    pub fn new(username: String) -> Self {
623        let secret = Uuid::new_v4().to_string();
624        let created_at = DateTime::from_system_time(SystemTime::now());
625
626        SetPasswordToken {
627            username,
628            secret,
629            created_at,
630        }
631    }
632}
633
634impl From<SetPasswordToken> for Bson {
635    fn from(token: SetPasswordToken) -> Bson {
636        Bson::Document(doc! {
637            "username": token.username,
638            "secret": token.secret,
639            "createdAt": token.created_at,
640        })
641    }
642}
643
644#[derive(Deserialize, Serialize, Debug, Clone)]
645#[serde(rename_all = "camelCase")]
646pub struct AuthorizedServiceHost {
647    pub url: String,
648    pub id: String,
649    pub visibility: ServiceHostScope,
650    pub secret: String,
651}
652
653impl AuthorizedServiceHost {
654    pub fn new(url: String, id: String, visibility: ServiceHostScope) -> Self {
655        let secret = Uuid::new_v4().to_string();
656        AuthorizedServiceHost {
657            url,
658            id,
659            secret,
660            visibility,
661        }
662    }
663
664    pub fn auth_header(&self) -> (&'static str, String) {
665        let token = self.id.clone() + ":" + &self.secret;
666        ("X-Authorization", token)
667    }
668}
669
670impl From<AuthorizedServiceHost> for Bson {
671    fn from(host: AuthorizedServiceHost) -> Bson {
672        Bson::Document(doc! {
673            "url": host.url,
674            "id": host.id,
675            "visibility": host.visibility,
676            "secret": host.secret,
677        })
678    }
679}
680
681impl From<netsblox_api_common::AuthorizedServiceHost> for AuthorizedServiceHost {
682    fn from(data: netsblox_api_common::AuthorizedServiceHost) -> AuthorizedServiceHost {
683        AuthorizedServiceHost::new(data.url, data.id, data.visibility)
684    }
685}
686
687impl From<AuthorizedServiceHost> for netsblox_api_common::AuthorizedServiceHost {
688    fn from(host: AuthorizedServiceHost) -> netsblox_api_common::AuthorizedServiceHost {
689        netsblox_api_common::AuthorizedServiceHost {
690            id: host.id,
691            url: host.url,
692            visibility: host.visibility,
693        }
694    }
695}
696
697impl From<AuthorizedServiceHost> for netsblox_api_common::ServiceHost {
698    fn from(host: AuthorizedServiceHost) -> netsblox_api_common::ServiceHost {
699        let categories = match host.visibility {
700            ServiceHostScope::Public(cats) => cats,
701            ServiceHostScope::Private => Vec::new(),
702        };
703
704        netsblox_api_common::ServiceHost {
705            url: host.url,
706            categories,
707        }
708    }
709}
710
711#[derive(Serialize, Deserialize, Clone)]
712#[serde(rename_all = "camelCase")]
713pub struct Library {
714    pub owner: String,
715    pub name: String,
716    pub notes: String,
717    pub blocks: String,
718    pub state: PublishState,
719}
720
721impl From<Library> for LibraryMetadata {
722    fn from(library: Library) -> LibraryMetadata {
723        LibraryMetadata::new(
724            library.owner.clone(),
725            library.name.clone(),
726            library.state,
727            Some(library.notes),
728        )
729    }
730}
731
732impl From<Library> for Bson {
733    fn from(library: Library) -> Self {
734        Bson::Document(doc! {
735            "owner": library.owner,
736            "name": library.name,
737            "notes": library.notes,
738            "blocks": library.blocks,
739            "state": library.state,
740        })
741    }
742}
743
744#[derive(Serialize, Deserialize)]
745#[serde(rename_all = "camelCase")]
746pub struct OAuthClient {
747    pub id: oauth::ClientId,
748    pub name: String,
749    created_at: DateTime,
750    hash: String,
751    salt: String,
752}
753
754impl OAuthClient {
755    pub fn new(name: String, password: String) -> Self {
756        let salt = passwords::PasswordGenerator::new()
757            .length(8)
758            .exclude_similar_characters(true)
759            .numbers(true)
760            .spaces(false)
761            .generate_one()
762            .unwrap_or_else(|_err| "salt".to_owned());
763
764        let hash = sha512(&(password + &salt));
765        Self {
766            id: oauth::ClientId::new(Uuid::new_v4().to_string()),
767            name,
768            created_at: DateTime::from_system_time(SystemTime::now()),
769            hash,
770            salt,
771        }
772    }
773}
774
775impl From<OAuthClient> for Bson {
776    fn from(client: OAuthClient) -> Bson {
777        Bson::Document(doc! {
778            "id": client.id,
779            "name": client.name,
780            "createdAt": client.created_at,
781            "hash": client.hash,
782            "salt": client.salt,
783        })
784    }
785}
786
787impl From<OAuthClient> for oauth::Client {
788    fn from(client: OAuthClient) -> oauth::Client {
789        oauth::Client {
790            id: client.id,
791            name: client.name,
792        }
793    }
794}
795
796#[derive(Serialize, Deserialize)]
797#[serde(rename_all = "camelCase")]
798pub struct OAuthToken {
799    pub id: oauth::TokenId,
800    pub client_id: oauth::ClientId,
801    pub username: String,
802    pub created_at: DateTime,
803}
804
805impl OAuthToken {
806    pub fn new(client_id: oauth::ClientId, username: String) -> Self {
807        let id = oauth::TokenId::new(Uuid::new_v4().to_string());
808        let created_at = DateTime::from_system_time(SystemTime::now());
809
810        Self {
811            id,
812            client_id,
813            username,
814            created_at,
815        }
816    }
817}
818
819impl From<OAuthToken> for oauth::Token {
820    fn from(token: OAuthToken) -> oauth::Token {
821        oauth::Token {
822            id: token.id,
823            client_id: token.client_id,
824            username: token.username,
825            created_at: token.created_at.to_system_time(),
826        }
827    }
828}
829
830impl From<OAuthToken> for Bson {
831    fn from(token: OAuthToken) -> Bson {
832        Bson::Document(doc! {
833            "id": token.id,
834            "client_id": token.client_id,
835            "username": token.username,
836            "createdAt": token.created_at,
837        })
838    }
839}
840
841pub(crate) fn sha512(text: &str) -> String {
842    let mut hasher = Sha512::new();
843    hasher.update(text);
844    let hash = hasher.finalize();
845    hex::encode(hash)
846}
847
848/// A magic link is used for password-less login. It has no
849/// api version since exposing it via the api would be a pretty
850/// serious security vulnerability.
851#[derive(Serialize, Deserialize, Clone, Debug)]
852#[serde(rename_all = "camelCase")]
853pub struct MagicLink {
854    pub id: api::MagicLinkId,
855    pub email: String,
856    pub created_at: DateTime,
857}
858
859impl MagicLink {
860    pub fn new(email: String) -> Self {
861        Self {
862            id: api::MagicLinkId::new(Uuid::new_v4().to_string()),
863            email,
864            created_at: DateTime::now(),
865        }
866    }
867}
868
869impl From<MagicLink> for Bson {
870    fn from(link: MagicLink) -> Bson {
871        Bson::Document(doc! {
872            "id": link.id,
873            "email": link.email,
874            "createdAt": link.created_at,
875        })
876    }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882
883    #[test]
884    fn test_dont_schedule_deletion_for_saved_projects() {
885        let metadata =
886            ProjectMetadata::new("owner", "someProject", HashMap::new(), SaveState::Saved);
887        assert!(metadata.delete_at.is_none());
888    }
889
890    #[test]
891    fn test_schedule_deletion_for_created_projects() {
892        // This gives them 10 minutes to be occupied before deletion
893        let metadata =
894            ProjectMetadata::new("owner", "someProject", HashMap::new(), SaveState::Created);
895        assert!(metadata.delete_at.is_some());
896    }
897
898    #[test]
899    fn test_pub_auth_host_to_host_preserves_cats() {
900        let categories = vec!["cat1".into()];
901        let auth_host = AuthorizedServiceHost {
902            url: "http://localhost:8000".into(),
903            id: "SomeTrustedHost".into(),
904            secret: "SomeSecret".into(),
905            visibility: ServiceHostScope::Public(categories.clone()),
906        };
907        let host: ServiceHost = auth_host.into();
908
909        assert_eq!(host.categories.len(), 1);
910        assert_eq!(&host.categories.into_iter().next().unwrap(), "cat1");
911    }
912
913    #[test]
914    fn test_priv_auth_host_to_host_no_cats() {
915        let auth_host = AuthorizedServiceHost {
916            url: "http://localhost:8000".into(),
917            id: "SomeTrustedHost".into(),
918            secret: "SomeSecret".into(),
919            visibility: ServiceHostScope::Private,
920        };
921        let host: ServiceHost = auth_host.into();
922        assert_eq!(host.categories.len(), 0);
923    }
924}