/// <summary> /// Initializes a new instance of the <see cref="Tags"/> class. /// </summary> /// <param name="accessRepo">Supplied through DI.</param> /// <param name="sessionRepo">Supplied through DI.</param> /// <param name="date">Supplied through DI.</param> public Tags( IRepository <AccessTokenSchema> accessRepo, IRepository <SessionSchema> sessionRepo, IDateExtra date) : base("/sessions/{id}/tags") { this.Before += PreSecurity.CheckAccess(accessRepo, date); this.Post <PostTags>("/{tagName}", async(req, res) => { string shortID = req.RouteValues.As <string>("id"); var sessionDoc = await sessionRepo .FindById(shortID); if (sessionDoc == null) { res.StatusCode = Status404NotFound; return; } string tag = req.RouteValues.As <string>("tagName"); if (!sessionDoc.Tags.Contains(tag)) { sessionDoc.Tags.Add(tag); var filter = Builders <SessionSchema> .Filter .Where(s => s.Id == shortID); var update = Builders <SessionSchema> .Update .Set(s => s.Tags, sessionDoc.Tags); await sessionRepo.Update(filter, update); } await res.FromString(); }); this.Delete <DeleteTags>("/{tagName}", async(req, res) => { string shortID = req.RouteValues.As <string>("id"); var sessionDoc = await sessionRepo .FindById(shortID); if (sessionDoc == null) { res.StatusCode = Status404NotFound; return; } string tag = req.RouteValues.As <string>("tagName"); if (sessionDoc.Tags.Contains(tag)) { sessionDoc.Tags.Remove(tag); var filter = Builders <SessionSchema> .Filter .Where(s => s.Id == shortID); var update = Builders <SessionSchema> .Update .Set(s => s.Tags, sessionDoc.Tags); await sessionRepo.Update(filter, update); } await res.FromString(); }); }
/// <summary> /// Initializes a new instance of the <see cref="ProtectedUsersAndAuthentication"/> class. /// </summary> /// <param name="accessRepo">Supplied through DI.</param> /// <param name="signUpRepo">Supplied through DI.</param> /// <param name="personRepo">Supplied through DI.</param> /// <param name="env">Supplied through DI.</param> /// <param name="date">Supplied through DI.</param> public ProtectedUsersAndAuthentication( IRepository <AccessTokenSchema> accessRepo, IRepository <SignUpTokenSchema> signUpRepo, IRepository <PersonSchema> personRepo, IAppEnvironment env, IDateExtra date) : base("/") { this.Before += PreSecurity.CheckAccess(accessRepo, date); this.Get <GetUser>("users/{id}/", async(req, res) => { string userID = req.RouteValues.As <string>("id"); var person = await personRepo.FindById(userID); if (person == null || !person.IsExisting) { res.StatusCode = Status404NotFound; return; } // Has to exit due to pre-security check string token = req.Cookies["ExperienceCapture-Access-Token"]; var accessToken = await accessRepo.FindOne( Builders <AccessTokenSchema> .Filter .Where(a => a.Hash == PasswordHasher.Hash(token))); if (person.InternalId != accessToken.User && accessToken.Role != RoleOptions.Admin) { res.StatusCode = Status401Unauthorized; return; } string json = JsonQuery.FulfilEncoding(req.Query, person); if (json != null) { await res.FromJson(json); return; } person.InternalId = null; await res.FromBson(person); }); this.Delete <DeleteUser>("users/{id}/", async(req, res) => { string userID = req.RouteValues.As <string>("id"); var person = await personRepo.FindById(userID); if (person == null) { res.StatusCode = Status404NotFound; return; } // Has to exit due to pre-security check string token = req.Cookies["ExperienceCapture-Access-Token"]; var accessFilter = Builders <AccessTokenSchema> .Filter .Where(a => a.Hash == PasswordHasher.Hash(token)); var accessToken = await accessRepo.FindOne(accessFilter); // Check if the user being requested is the same // As the one requesting, unless they have the admin role if (person.InternalId != accessToken.User && accessToken.Role != RoleOptions.Admin) { res.StatusCode = Status401Unauthorized; return; } var filter = Builders <PersonSchema> .Filter .Where(p => p.Id == userID); var update = Builders <PersonSchema> .Update .Set(p => p.IsExisting, false); await personRepo.Update(filter, update); await res.FromString(); }); this.Post <PostSignUp>("authentication/signUps", async(req, res) => { string newToken = Generate.GetRandomToken(); var tokenDoc = new SignUpTokenSchema { InternalId = ObjectId.GenerateNewId(), Hash = PasswordHasher.Hash(newToken), CreatedAt = new BsonDateTime(date.UtcNow), }; await signUpRepo.Add(tokenDoc); var responce = new SignUpTokenResponce { SignUpToken = newToken, Expiration = TimerExtra.ProjectSeconds(date, tokenDoc.ExpirationSeconds), }; var responceDoc = responce.ToBsonDocument(); string json = JsonQuery.FulfilEncoding(req.Query, responceDoc); if (json != null) { await res.FromJson(json); return; } await res.FromBson(responceDoc); }); }
/// <summary> /// Initializes a new instance of the <see cref="Export"/> class. /// </summary> /// <param name="accessRepo">Supplied through DI.</param> /// <param name="sessionRepo">Supplied through DI.</param> /// <param name="threader">Supplied through DI.</param> /// <param name="os">Supplied through DI.</param> /// <param name="date">Supplied through DI.</param> public Export( IRepository <AccessTokenSchema> accessRepo, IRepository <SessionSchema> sessionRepo, IThreadExtra threader, IMinioClient os, IDateExtra date) : base("/sessions/{id}/export") { this.Before += PreSecurity.CheckAccess(accessRepo, date); this.Post <PostExport>("/", async(req, res) => { string id = req.RouteValues.As <string>("id"); var sessionDoc = await sessionRepo .FindById(id); if (sessionDoc == null) { res.StatusCode = Status404NotFound; return; } var exporterConfig = new ExporterConfiguration { Mongo = new ConnectionConfiguration { ConnectionString = AppConfiguration.Mongo.ConnectionString, Port = AppConfiguration.Mongo.Port, }, Minio = new ConnectionConfiguration { ConnectionString = AppConfiguration.Minio.ConnectionString, Port = AppConfiguration.Minio.Port, }, Id = id, }; threader.Run(ExportHandler.Entry, exporterConfig); var filter = Builders <SessionSchema> .Filter .Where(s => s.Id == id); var update = Builders <SessionSchema> .Update .Set(s => s.ExportState, ExportOptions.Pending); await sessionRepo.Update(filter, update); await res.FromString(); }); this.Get <GetExport>("/", async(req, res) => { string id = req.RouteValues.As <string>("id"); var sessionDoc = await sessionRepo.FindById(id); if (sessionDoc == null) { res.StatusCode = Status404NotFound; return; } if (sessionDoc.ExportState == ExportOptions.Pending) { res.StatusCode = Status202Accepted; await res.FromString("PENDING"); return; } // Export job hasn't been created, so return not found if (sessionDoc.ExportState == ExportOptions.NotStarted) { res.StatusCode = Status404NotFound; return; } // Not sure what to do about an error if (sessionDoc.ExportState == ExportOptions.Error) { res.StatusCode = Status500InternalServerError; return; } string bucketName = "sessions.exported"; string objectName = $"{id}_session_exported.zip"; byte[] body = await os.GetBytesAsync(bucketName, objectName); var about = new ContentDisposition { FileName = objectName }; using (var stream = new MemoryStream(body)) { res.ContentLength = body.Length; await res.FromStream(stream, "application/zip", about); } }); }
/// <summary> /// Initializes a new instance of the <see cref="Sessions"/> class. /// </summary> /// <param name="accessRepo">Supplied through DI.</param> /// <param name="sessionRepo">Supplied through DI.</param> /// <param name="personRepo">Supplied through DI.</param> /// <param name="captureRepo">Supplied through DI.</param> /// <param name="logger">Supplied through DI.</param> /// <param name="date">Supplied through DI.</param> public Sessions( IRepository <AccessTokenSchema> accessRepo, IRepository <SessionSchema> sessionRepo, IRepository <PersonSchema> personRepo, IRepository <BsonDocument> captureRepo, ILogger logger, IDateExtra date) : base("/sessions") { this.Before += PreSecurity.CheckAccess(accessRepo, date); this.Post <PostSessions>("/", async(req, res) => { // Has to exist due to PreSecurity Check string token = req.Cookies["ExperienceCapture-Access-Token"]; var accessTokenDoc = await accessRepo.FindOne( Builders <AccessTokenSchema> .Filter .Where(a => a.Hash == PasswordHasher.Hash(token))); var filterUser = Builders <PersonSchema> .Filter.Where(p => p.InternalId == accessTokenDoc.User); var user = await personRepo .FindOne(filterUser); string shortID = Generate.GetRandomId(4); var sessionDoc = new SessionSchema { InternalId = ObjectId.GenerateNewId(), Id = shortID, User = user, // Copying user data instead of referencing so it can never change in the session CreatedAt = new BsonDateTime(date.UtcNow), Tags = new List <string>(), }; // Retry generating a short id until it is unique bool isUnique = true; do { try { sessionDoc.Id = shortID; await sessionRepo.Add(sessionDoc); isUnique = true; } catch (MongoWriteException e) { // Re-throw any other type of exception except non-unique keys if (e.WriteError.Code != 11000) { throw e; } shortID = Generate.GetRandomId(4); isUnique = false; } }while (!isUnique); // isOngoing is a proxy variable and will always start out as true sessionDoc.IsOngoing = true; sessionDoc.InternalId = null; sessionDoc.User.InternalId = null; captureRepo.Configure($"sessions.{shortID}"); // Secondary index or else Mongo will fail on large queries // It has a limit for max number of documents on properties // Without an index, see https://docs.mongodb.com/manual/reference/limits/#Sort-Operations var index = Builders <BsonDocument> .IndexKeys; var key = index.Ascending("frameInfo.realtimeSinceStartup"); await captureRepo.Index(key); string json = JsonQuery.FulfilEncoding(req.Query, sessionDoc); if (json != null) { await res.FromJson(json); return; } await res.FromBson(sessionDoc); }); this.Get <GetSessions>("/", async(req, res) => { var builder = Builders <SessionSchema> .Filter; // Note: only use `&=` for adding to the filter, // Or else the filter cannot handle multiple query string options FilterDefinition <SessionSchema> filter = builder.Empty; var startMin = new BsonDateTime(date.UtcNow.AddSeconds(-300)); // 5 minutes var closeMin = new BsonDateTime(date.UtcNow.AddSeconds(-5)); // 5 seconds var hasTags = req.Query.AsMultiple <string>("hasTags").ToList(); if (hasTags.Count > 0) { foreach (var tag in hasTags) { filter &= builder.Where(s => s.Tags.Contains(tag)); } } var lacksTags = req.Query.AsMultiple <string>("lacksTags").ToList(); if (lacksTags.Count > 0) { foreach (var tag in lacksTags) { filter &= builder.Where(s => !s.Tags.Contains(tag)); } } // Three potential options: null, true, or false if (req.Query.As <bool?>("isOngoing") != null) { bool isOngoing = req.Query.As <bool>("isOngoing"); if (isOngoing) { filter &= builder.Where(s => s.IsOpen == true) & ((builder.Eq(s => s.LastCaptureAt, BsonNull.Value) & builder.Where(s => s.CreatedAt > startMin)) | (builder.Eq(s => s.LastCaptureAt, BsonNull.Value) & builder.Where(s => s.LastCaptureAt > closeMin))); } else { filter &= builder.Where(s => s.IsOpen == false) | ((builder.Eq(s => s.LastCaptureAt, BsonNull.Value) & builder.Where(s => s.CreatedAt < startMin)) | (builder.Eq(s => s.LastCaptureAt, BsonNull.Value) & builder.Where(s => s.LastCaptureAt < closeMin))); } } var page = req.Query.As <int?>("page") ?? 1; if (page < 1) { // Page query needs to be possible res.StatusCode = Status400BadRequest; return; } var direction = req.Query.As <string>("sort"); SortDefinition <SessionSchema> sorter; if (direction == null) { sorter = Builders <SessionSchema> .Sort.Descending(s => s.CreatedAt); } else { if (Enum.TryParse(typeof(SortOptions), direction, true, out object options)) { sorter = ((SortOptions)options).ToDefinition(); } else { res.StatusCode = Status400BadRequest; return; } } var sessionDocs = await sessionRepo .FindAll(filter, sorter, page); var sessionsDocsWithOngoing = sessionDocs.Select((s) => { bool isStarted = false; if (s.LastCaptureAt != BsonNull.Value) { isStarted = true; } bool isOngoing; if (s.IsOpen) { isOngoing = (!isStarted && startMin < s.CreatedAt) || (isStarted && closeMin < s.LastCaptureAt); } else { isOngoing = false; } s.IsOngoing = isOngoing; s.InternalId = null; s.User.InternalId = null; return(s); }); var count = await sessionRepo.FindThenCount(filter); var clientValues = new SessionsResponce { // Bson documents can't start with an array like Json, so a wrapping object is used instead ContentList = sessionsDocsWithOngoing.ToList(), PageTotal = (long)Math.Ceiling((double)count / 10d), }; string json = JsonQuery.FulfilEncoding(req.Query, clientValues); if (json != null) { await res.FromJson(json); return; } await res.FromBson(clientValues); }); this.Post <PostSession>("/{id}", async(req, res) => { string shortID = req.RouteValues.As <string>("id"); var sessionDoc = await sessionRepo .FindById(shortID); if (sessionDoc == null) { res.StatusCode = Status404NotFound; return; } if (!sessionDoc.IsOpen) { res.StatusCode = Status400BadRequest; return; } BsonDocument document; if (JsonQuery.CheckDecoding(req.Query)) { using (var ms = new MemoryStream()) { await req.Body.CopyToAsync(ms); ms.Position = 0; try { document = BsonSerializer.Deserialize <BsonDocument>(ms); } catch (Exception err) { logger.LogError(err.Message); res.StatusCode = Status400BadRequest; return; } } } else { string json = await req.Body.AsStringAsync(); try { document = BsonDocument.Parse(json); } catch (Exception err) { logger.LogError(err.Message); res.StatusCode = Status400BadRequest; return; } } // Manual validation, because Fluent Validation would remove extra properties if (!document.Contains("frameInfo") || document["frameInfo"].BsonType != BsonType.Document || !document["frameInfo"].AsBsonDocument.Contains("realtimeSinceStartup") || document["frameInfo"]["realtimeSinceStartup"].BsonType != BsonType.Double) { res.StatusCode = Status400BadRequest; return; } captureRepo.Configure($"sessions.{shortID}"); await captureRepo.Add(document); var filter = Builders <SessionSchema> .Filter.Where(s => s.Id == shortID); // This lastCaptureAt is undefined on the session document until the first call of this endpoint // Export flags are reset so the session can be re-exported var update = Builders <SessionSchema> .Update .Set(s => s.LastCaptureAt, new BsonDateTime(date.UtcNow)) .Set(s => s.ExportState, ExportOptions.NotStarted); await sessionRepo.Update(filter, update); await res.FromString(); }); this.Get <GetSession>("/{id}", async(req, res) => { string shortID = req.RouteValues.As <string>("id"); var sessionDoc = await sessionRepo .FindById(shortID); if (sessionDoc == null) { res.StatusCode = Status404NotFound; return; } var startRange = new BsonDateTime(date.UtcNow.AddSeconds(-300)); // 5 minutes var closeRange = new BsonDateTime(date.UtcNow.AddSeconds(-5)); // 5 seconds bool isStarted = false; // Check if key exists if (sessionDoc.LastCaptureAt != BsonNull.Value) { isStarted = true; } bool isOngoing; if (sessionDoc.IsOpen) { isOngoing = (!isStarted && startRange.CompareTo(sessionDoc.CreatedAt) < 0) || (isStarted && closeRange.CompareTo(sessionDoc.LastCaptureAt) < 0); } else { isOngoing = false; } sessionDoc.IsOngoing = isOngoing; sessionDoc.InternalId = null; sessionDoc.User.InternalId = null; string json = JsonQuery.FulfilEncoding(req.Query, sessionDoc); if (json != null) { await res.FromJson(json); return; } await res.FromBson(sessionDoc); }); this.Delete <DeleteSession>("/{id}", async(req, res) => { string shortID = req.RouteValues.As <string>("id"); var filter = Builders <SessionSchema> .Filter.Where(s => s.Id == shortID); var sessionDoc = await sessionRepo .FindById(shortID); if (sessionDoc == null) { res.StatusCode = Status404NotFound; return; } var update = Builders <SessionSchema> .Update .Set(s => s.IsOpen, false); await sessionRepo.Update(filter, update); await res.FromString(); }); }