/// <summary> Get the link sharing key for a project if it exists, otherwise create a new one. </summary> public async Task <string> GetLinkSharingKeyAsync(string projectId, string role) { SFProject project = await GetProjectAsync(projectId); if (!(project.CheckingConfig.ShareEnabled && project.CheckingConfig.ShareLevel == CheckingShareLevel.Anyone)) { return(null); } SFProjectSecret projectSecret = await ProjectSecrets.GetAsync(projectId); // Link sharing keys have Email set to null and ExpirationTime set to null. string key = projectSecret.ShareKeys.SingleOrDefault( sk => sk.Email == null && sk.ProjectRole == role)?.Key; if (!string.IsNullOrEmpty(key)) { return(key); } // Generate a new link sharing key for the given role key = _securityService.GenerateKey(); await ProjectSecrets.UpdateAsync(p => p.Id == projectId, update => update.Add(p => p.ShareKeys, new ShareKey { Key = key, ProjectRole = role, ExpirationTime = null } )); return(key); }
/// <summary> Check that a share link is valid for a project and add the user to the project. </summary> public async Task CheckLinkSharingAsync(string curUserId, string projectId, string shareKey) { using (IConnection conn = await RealtimeService.ConnectAsync(curUserId)) { IDocument <SFProject> projectDoc = await GetProjectDocAsync(projectId, conn); if (projectDoc.Data.UserRoles.ContainsKey(curUserId)) { return; } IDocument <User> userDoc = await conn.FetchAsync <User>(curUserId); string projectRole; // Attempt to get the role for the user from the Paratext registry Attempt <string> attempt = await TryGetProjectRoleAsync(projectDoc.Data, curUserId); if (!attempt.TryResult(out projectRole)) { // Get the project role that is specified in the sharekey Attempt <SFProjectSecret> psAttempt = await ProjectSecrets.TryGetAsync(projectId); if (psAttempt.TryResult(out SFProjectSecret ps)) { projectRole = ps.ShareKeys.SingleOrDefault(sk => sk.Key == shareKey)?.ProjectRole; } } // The share key was invalid if (projectRole == null) { throw new ForbiddenException(); } bool linkSharing = projectDoc.Data.CheckingConfig.ShareEnabled && projectDoc.Data.CheckingConfig.ShareLevel == CheckingShareLevel.Anyone; if (linkSharing) { // Add the user and remove the specific user share key if it exists. Link sharing keys // have Email set to null and will not be removed. await AddUserToProjectAsync(conn, projectDoc, userDoc, projectRole, true); return; } // Look for a valid specific user share key. SFProjectSecret projectSecret = await ProjectSecrets.UpdateAsync( p => p.Id == projectId && p.ShareKeys.Any( sk => sk.Email != null && sk.Key == shareKey && sk.ExpirationTime > DateTime.UtcNow), update => update.RemoveAll(p => p.ShareKeys, sk => sk.Key == shareKey) ); if (projectSecret != null) { await AddUserToProjectAsync(conn, projectDoc, userDoc, projectRole, false); return; } throw new ForbiddenException(); } }
public async Task DeleteProjectAsync(string curUserId, string projectId) { string ptProjectId; using (IConnection conn = await RealtimeService.ConnectAsync(curUserId)) { IDocument <SFProject> projectDoc = await conn.FetchAsync <SFProject>(projectId); if (!projectDoc.IsLoaded) { throw new DataNotFoundException("The project does not exist."); } if (!IsProjectAdmin(projectDoc.Data, curUserId)) { throw new ForbiddenException(); } ptProjectId = projectDoc.Data.ParatextId; // delete the project first, so that users get notified about the deletion string[] projectUserIds = projectDoc.Data.UserRoles.Keys.ToArray(); await projectDoc.DeleteAsync(); async Task removeUser(string projectUserId) { IDocument <User> userDoc = await conn.FetchAsync <User>(projectUserId); await RemoveUserFromProjectAsync(conn, projectDoc, userDoc); } var tasks = new List <Task>(); foreach (string projectUserId in projectUserIds) { tasks.Add(removeUser(projectUserId)); } await Task.WhenAll(tasks); } await ProjectSecrets.DeleteAsync(projectId); await RealtimeService.DeleteProjectAsync(projectId); await _engineService.RemoveProjectAsync(projectId); string projectDir = Path.Combine(SiteOptions.Value.SiteDir, "sync", ptProjectId); if (FileSystemService.DirectoryExists(projectDir)) { FileSystemService.DeleteDirectory(projectDir); } string audioDir = GetAudioDir(projectId); if (FileSystemService.DirectoryExists(audioDir)) { FileSystemService.DeleteDirectory(audioDir); } }
protected virtual async Task AddUserToProjectAsync(IConnection conn, IDocument <TModel> projectDoc, IDocument <User> userDoc, string projectRole, bool removeShareKeys = true) { await projectDoc.SubmitJson0OpAsync(op => op.Set(p => p.UserRoles[userDoc.Id], projectRole)); if (removeShareKeys) { await ProjectSecrets.UpdateAsync(p => p.Id == projectDoc.Id, update => update.RemoveAll(p => p.ShareKeys, sk => sk.Email == userDoc.Data.Email)); } string siteId = SiteOptions.Value.Id; await userDoc.SubmitJson0OpAsync(op => op.Add(u => u.Sites[siteId].Projects, projectDoc.Id)); }
/// <summary>Return list of email addresses with outstanding invitations</summary> public async Task <string[]> InvitedUsersAsync(string curUserId, string projectId) { SFProject project = await GetProjectAsync(projectId); if (!IsProjectAdmin(project, curUserId)) { throw new ForbiddenException(); } SFProjectSecret projectSecret = await ProjectSecrets.GetAsync(projectId); return(projectSecret.ShareKeys.Select(sk => sk.Email).ToArray()); }
/// <summary>Is there already a pending invitation to the project for the specified email address?</summary> public async Task <bool> IsAlreadyInvitedAsync(string curUserId, string projectId, string email) { SFProject project = await GetProjectAsync(projectId); if (!IsProjectAdmin(project, curUserId) && !project.CheckingConfig.ShareEnabled) { throw new ForbiddenException(); } if (email == null) { return(false); } return(await ProjectSecrets.Query() .AnyAsync(p => p.Id == projectId && p.ShareKeys.Any(sk => sk.Email == email))); }
public async Task <bool> InviteAsync(string curUserId, string projectId, string email) { SFProject project = await GetProjectAsync(projectId); if (await RealtimeService.QuerySnapshots <User>() .AnyAsync(u => project.UserRoles.Keys.Contains(u.Id) && u.Email == email)) { return(false); } SiteOptions siteOptions = SiteOptions.Value; if (!project.CheckingConfig.ShareEnabled && !IsProjectAdmin(project, curUserId)) { throw new ForbiddenException(); } // Invite a specific person. Reuse prior code, if any. SFProjectSecret projectSecret = await ProjectSecrets.UpdateAsync( p => p.Id == projectId && !p.ShareKeys.Any(sk => sk.Email == email), update => update.Add(p => p.ShareKeys, new ShareKey { Email = email, Key = _securityService.GenerateKey() })); if (projectSecret == null) { projectSecret = await ProjectSecrets.GetAsync(projectId); } string key = projectSecret.ShareKeys.Single(sk => sk.Email == email).Key; string url = $"{siteOptions.Origin}projects/{projectId}?sharing=true&shareKey={key}"; string emailSpecificLinkMessage = _localizer[SharedResource.Keys.InviteLinkSharingOff]; User inviter = await RealtimeService.GetSnapshotAsync <User>(curUserId); string subject = _localizer[SharedResource.Keys.InviteSubject, project.Name, siteOptions.Name]; var greeting = $"<p>{_localizer[SharedResource.Keys.InviteGreeting, "<p>", inviter.Name, project.Name, siteOptions.Name, $"<a href=\"{url}\">{url}</a><p>"]}"; var instructions = $"<p>{_localizer[SharedResource.Keys.InviteInstructions, siteOptions.Name, "<b>", "</b>"]}"; var pt = $"<ul><li>{_localizer[SharedResource.Keys.InvitePTOption, "<b>", "</b>", siteOptions.Name]}</li>"; var google = $"<li>{_localizer[SharedResource.Keys.InviteGoogleOption, "<b>", "</b>", siteOptions.Name]}</li>"; var facebook = $"<li>{_localizer[SharedResource.Keys.InviteFacebookOption, "<b>", "</b>", siteOptions.Name]}</li>"; var withemail = $"<li>{_localizer[SharedResource.Keys.InviteEmailOption, siteOptions.Name]}</li></ul></p><p></p>"; var signoff = $"<p>{_localizer[SharedResource.Keys.InviteSignature, "<p>", siteOptions.Name]}</p>"; var emailBody = $"{greeting}{emailSpecificLinkMessage}{instructions}{pt}{google}{facebook}{withemail}{signoff}"; await _emailService.SendEmailAsync(email, subject, emailBody); return(true); }
/// <summary>Return list of email addresses with outstanding invitations</summary> public async Task <IReadOnlyList <InviteeStatus> > InvitedUsersAsync(string curUserId, string projectId) { SFProject project = await GetProjectAsync(projectId); if (!IsProjectAdmin(project, curUserId)) { throw new ForbiddenException(); } SFProjectSecret projectSecret = await ProjectSecrets.GetAsync(projectId); DateTime now = DateTime.UtcNow; return(projectSecret.ShareKeys.Where(s => s.Email != null).Select(sk => new InviteeStatus { Email = sk.Email, Expired = sk.ExpirationTime < now }).ToArray()); }
public async Task CheckLinkSharingAsync(string curUserId, string projectId, string shareKey = null) { using (IConnection conn = await RealtimeService.ConnectAsync(curUserId)) { IDocument <SFProject> projectDoc = await GetProjectDocAsync(projectId, conn); if (projectDoc.Data.UserRoles.ContainsKey(curUserId)) { return; } IDocument <User> userDoc = await conn.FetchAsync <User>(curUserId); Attempt <string> attempt = await TryGetProjectRoleAsync(projectDoc.Data, curUserId); string projectRole = attempt.Result; if (shareKey != null) { string currentUserEmail = userDoc.Data.Email; SFProjectSecret projectSecret = await ProjectSecrets.UpdateAsync( p => p.Id == projectId && p.ShareKeys.Any(sk => sk.Email == currentUserEmail && sk.Key == shareKey), update => update.RemoveAll(p => p.ShareKeys, sk => sk.Email == currentUserEmail)); if (projectSecret != null) { await AddUserToProjectAsync(conn, projectDoc, userDoc, projectRole, false); return; } } if (projectDoc.Data.CheckingConfig.ShareEnabled == true && projectDoc.Data.CheckingConfig.ShareLevel == CheckingShareLevel.Anyone) { // Users with the project link get added to the project. This also covers the case where // a user was emailed a share key and the invite was cancelled, but link sharing is enabled await AddUserToProjectAsync(conn, projectDoc, userDoc, projectRole); return; } throw new ForbiddenException(); } }
/// <summary>Cancel an outstanding project invitation.</summary> public async Task UninviteUserAsync(string curUserId, string projectId, string emailToUninvite) { SFProject project = await GetProjectAsync(projectId); if (!IsProjectAdmin(project, curUserId)) { throw new ForbiddenException(); } if (!await IsAlreadyInvitedAsync(curUserId, projectId, emailToUninvite)) { // There is not an invitation for this email address return; } await ProjectSecrets.UpdateAsync(projectId, u => { u.RemoveAll(secretSet => secretSet.ShareKeys, shareKey => shareKey.Email == (emailToUninvite)); }); }
/// <summary> /// Returns SF project id of created project. /// </summary> public async Task <string> CreateProjectAsync(string curUserId, SFProjectCreateSettings settings) { Attempt <UserSecret> userSecretAttempt = await _userSecrets.TryGetAsync(curUserId); if (!userSecretAttempt.TryResult(out UserSecret userSecret)) { throw new DataNotFoundException("The user does not exist."); } IReadOnlyList <ParatextProject> ptProjects = await _paratextService.GetProjectsAsync(userSecret); ParatextProject ptProject = ptProjects.SingleOrDefault(p => p.ParatextId == settings.ParatextId); if (ptProject == null) { throw new DataNotFoundException("The paratext project does not exist."); } var project = new SFProject { ParatextId = settings.ParatextId, Name = ptProject.Name, ShortName = ptProject.ShortName, WritingSystem = new WritingSystem { Tag = ptProject.LanguageTag }, TranslateConfig = new TranslateConfig { TranslationSuggestionsEnabled = settings.TranslationSuggestionsEnabled }, CheckingConfig = new CheckingConfig { CheckingEnabled = settings.CheckingEnabled } }; Attempt <string> attempt = await TryGetProjectRoleAsync(project, curUserId); if (!attempt.TryResult(out string projectRole) || projectRole != SFProjectRole.Administrator) { throw new ForbiddenException(); } string projectId = ObjectId.GenerateNewId().ToString(); using (IConnection conn = await RealtimeService.ConnectAsync(curUserId)) { if (this.RealtimeService.QuerySnapshots <SFProject>().Any( (SFProject sfProject) => sfProject.ParatextId == project.ParatextId)) { throw new InvalidOperationException(ErrorAlreadyConnectedKey); } IDocument <SFProject> projectDoc = await conn.CreateAsync <SFProject>(projectId, project); await ProjectSecrets.InsertAsync(new SFProjectSecret { Id = projectDoc.Id }); IDocument <User> userDoc = await conn.FetchAsync <User>(curUserId); await AddUserToProjectAsync(conn, projectDoc, userDoc, SFProjectRole.Administrator, false); // Add the source after the project has been created // This will make the source project appear after the target, if it needs to be created if (settings.SourceParatextId != null && settings.SourceParatextId != settings.ParatextId) { TranslateSource source = await this.GetTranslateSourceAsync( curUserId, userSecret, settings.SourceParatextId, ptProjects); await projectDoc.SubmitJson0OpAsync(op => { UpdateSetting(op, p => p.TranslateConfig.Source, source); }); } if (projectDoc.Data.TranslateConfig.TranslationSuggestionsEnabled) { var machineProject = new MachineProject { Id = projectDoc.Id, SourceLanguageTag = projectDoc.Data.TranslateConfig.Source.WritingSystem.Tag, TargetLanguageTag = projectDoc.Data.WritingSystem.Tag }; await _engineService.AddProjectAsync(machineProject); } } await _syncService.SyncAsync(curUserId, projectId, true); return(projectId); }
public async Task <bool> InviteAsync(string curUserId, string projectId, string email, string locale, string role) { SFProject project = await GetProjectAsync(projectId); if (await RealtimeService.QuerySnapshots <User>() .AnyAsync(u => project.UserRoles.Keys.Contains(u.Id) && u.Email == email)) { return(false); } SiteOptions siteOptions = SiteOptions.Value; if (!project.CheckingConfig.ShareEnabled && !IsProjectAdmin(project, curUserId)) { throw new ForbiddenException(); } CultureInfo.CurrentUICulture = new CultureInfo(locale); // Remove the user sharekey if expired await ProjectSecrets.UpdateAsync( p => p.Id == projectId, update => update.RemoveAll(p => p.ShareKeys, sk => sk.Email == email && sk.ExpirationTime < DateTime.UtcNow) ); DateTime expTime = DateTime.UtcNow.AddDays(14); // Invite a specific person. Reuse prior code, if any. SFProjectSecret projectSecret = await ProjectSecrets.UpdateAsync( p => p.Id == projectId && !p.ShareKeys.Any(sk => sk.Email == email), update => update.Add(p => p.ShareKeys, new ShareKey { Email = email, Key = _securityService.GenerateKey(), ExpirationTime = expTime, ProjectRole = role } ) ); if (projectSecret == null) { projectSecret = await ProjectSecrets.GetAsync(projectId); int index = projectSecret.ShareKeys.FindIndex(sk => sk.Email == email); // Renew the expiration time of the valid key await ProjectSecrets.UpdateAsync( p => p.Id == projectId && p.ShareKeys.Any(sk => sk.Email == email), update => update.Set(p => p.ShareKeys[index].ExpirationTime, expTime) ); } string key = projectSecret.ShareKeys.Single(sk => sk.Email == email).Key; string url = $"{siteOptions.Origin}projects/{projectId}?sharing=true&shareKey={key}&locale={locale}"; string linkExpires = _localizer[SharedResource.Keys.InviteLinkExpires]; User inviter = await RealtimeService.GetSnapshotAsync <User>(curUserId); string subject = _localizer[SharedResource.Keys.InviteSubject, project.Name, siteOptions.Name]; var greeting = $"<p>{_localizer[SharedResource.Keys.InviteGreeting, "<p>", inviter.Name, project.Name, siteOptions.Name, $"<a href=\"{url}\">{url}</a><p>"]}"; var instructions = $"<p>{_localizer[SharedResource.Keys.InviteInstructions, siteOptions.Name, "<b>", "</b>"]}"; var pt = $"<ul><li>{_localizer[SharedResource.Keys.InvitePTOption, "<b>", "</b>", siteOptions.Name]}</li>"; var google = $"<li>{_localizer[SharedResource.Keys.InviteGoogleOption, "<b>", "</b>", siteOptions.Name]}</li>"; var facebook = $"<li>{_localizer[SharedResource.Keys.InviteFacebookOption, "<b>", "</b>", siteOptions.Name]}</li>"; var withemail = $"<li>{_localizer[SharedResource.Keys.InviteEmailOption, siteOptions.Name]}</li></ul></p><p></p>"; var signoff = $"<p>{_localizer[SharedResource.Keys.InviteSignature, "<p>", siteOptions.Name]}</p>"; var emailBody = $"{greeting}{linkExpires}{instructions}{pt}{google}{facebook}{withemail}{signoff}"; await _emailService.SendEmailAsync(email, subject, emailBody); return(true); }