Exemplo n.º 1
0
        /// <summary>
        /// Checks the access token, using a request and responce.
        /// </summary>
        /// <returns>
        /// True if the access token is valid.
        /// If false, the error status code is returned by pointer on the responce.
        /// </returns>
        /// <param name="req">An HTTP request.</param>
        /// <param name="res">An corresponding HTTP responce.</param>
        /// <param name="repo">Provides the access collection.</param>
        /// <param name="date">Provides the datetime.</param>
        /// <param name="minimumRole">The minimum role needed to access a module.</param>
        private static async Task <bool> Check(
            HttpRequest req,
            HttpResponse res,
            IRepository <AccessTokenSchema> repo,
            IDateExtra date,
            RoleOptions minimumRole = RoleOptions.Normal)
        {
            string token = req.Cookies["ExperienceCapture-Access-Token"];

            if (token == null)
            {
                res.StatusCode = Status400BadRequest;
                return(false);
            }

            var accessTokenDoc = await repo.FindOne(
                Builders <AccessTokenSchema>
                .Filter
                .Where(a => a.Hash == PasswordHasher.Hash(token)));

            if (accessTokenDoc == null ||
                accessTokenDoc.CreatedAt.IsAfter(date, accessTokenDoc.ExpirationSeconds) ||
                (int)accessTokenDoc.Role < (int)minimumRole)
            {
                res.StatusCode = Status401Unauthorized;
                return(false);
            }

            return(true);
        }
Exemplo n.º 2
0
 /// <summary>
 /// A wrapper for the check static method, it keep the interface clean.
 /// </summary>
 /// <returns>
 /// True if the access token is valid.
 /// If false, the error status code is returned by pointer on the responce.
 /// </returns>
 /// <param name="req">An HTTP request.</param>
 /// <param name="res">An corresponding HTTP responce.</param>
 /// <param name="repo">Provides the access collection.</param>
 /// <param name="date">Provides the datetime.</param>
 /// <param name="minimumRole">The minimum role needed to access a module.</param>
 public static async Task <bool> CheckAccessDirectly(
     HttpRequest req,
     HttpResponse res,
     IRepository <AccessTokenSchema> repo,
     IDateExtra date,
     RoleOptions minimumRole = RoleOptions.Normal)
 {
     return(await Check(req, res, repo, date, minimumRole));
 }
Exemplo n.º 3
0
 /// <summary>
 /// Checks the access token.
 /// </summary>
 /// <returns>
 /// A function that returns true if the access token is valid.
 /// </returns>
 /// <param name="repo">Provides the access collection.</param>
 /// <param name="date">Provides the datetime.</param>
 /// <param name="minimumRole">The minimum role needed to access a module.</param>
 public static Func <HttpContext, Task <bool> > CheckAccess(
     IRepository <AccessTokenSchema> repo,
     IDateExtra date,
     RoleOptions minimumRole = RoleOptions.Normal)
 {
     return((ctx) =>
     {
         return Check(ctx.Request, ctx.Response, repo, date, minimumRole);
     });
 }
Exemplo n.º 4
0
        /// <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();
            });
        }
Exemplo n.º 5
0
        /// <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);
                }
            });
        }
Exemplo n.º 6
0
 /// <summary>
 /// Adds seconds to the current time.
 /// </summary>
 /// <returns>
 /// A projected date in the future.
 /// </returns>
 /// <param name="date">Provides the current date.</param>
 /// <param name="expirationTime">An offset to be added on.</param>
 public static BsonDateTime ProjectSeconds(IDateExtra date, int expirationTime)
 {
     return(new BsonDateTime(date.UtcNow.AddSeconds(expirationTime)));
 }
Exemplo n.º 7
0
        /// <summary>
        /// Checks whether a time has expired.
        /// </summary>
        /// <returns>
        /// True if the start date and offset combined is after the current date.
        /// </returns>
        /// <param name="start">When to start counting from.</param>
        /// <param name="date">Provides the current date.</param>
        /// <param name="expirationTime">An offset to be added on.</param>
        public static bool IsAfter(this BsonDateTime start, IDateExtra date, int expirationTime)
        {
            BsonDateTime endTime = new BsonDateTime(date.UtcNow.AddSeconds(-expirationTime));

            return(start.CompareTo(endTime) < 0);
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="UsersAndAuthentication"/> class.
        /// </summary>
        /// <param name="accessRepo">Supplied through DI.</param>
        /// <param name="signUpRepo">Supplied through DI.</param>
        /// <param name="claimRepo">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 UsersAndAuthentication(
            IRepository <AccessTokenSchema> accessRepo,
            IRepository <SignUpTokenSchema> signUpRepo,
            IRepository <ClaimTokenSchema> claimRepo,
            IRepository <PersonSchema> personRepo,
            IAppEnvironment env,
            IDateExtra date)
            : base("/")
        {
            this.Post <PostUsers>("users", async(req, res) =>
            {
                var newPerson = await req.BindAndValidate <PersonRequest>();

                if (!newPerson.ValidationResult.IsValid)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                var person = await GoogleApi.ValidateUser(newPerson.Data.idToken, env);
                if (person == null)
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                var filter = Builders <SignUpTokenSchema>
                             .Filter
                             .Where(t => t.Hash == PasswordHasher.Hash(newPerson.Data.signUpToken));

                var signUpDoc = await signUpRepo.FindOne(filter);

                if (signUpDoc == null || signUpDoc.CreatedAt.IsAfter(date, signUpDoc.ExpirationSeconds))
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                if (!signUpDoc.IsExisting)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                var update = Builders <SignUpTokenSchema> .Update
                             .Set(s => s.IsExisting, false);

                var existingPerson = await personRepo.FindById(person.Subject);

                // Check if the person already exists
                if (existingPerson != null)
                {
                    // Recreate them if they where deleted
                    if (!existingPerson.IsExisting)
                    {
                        var filterPerson = Builders <PersonSchema> .Filter
                                           .Where(p => p.Id == person.Subject);

                        var updatePerson = Builders <PersonSchema> .Update
                                           .Set(p => p.IsExisting, true);

                        await personRepo.Update(filterPerson, updatePerson);
                        await signUpRepo.Update(filter, update);

                        await res.FromString();
                    }
                    else
                    {
                        // Return error is they really exist
                        res.StatusCode = Status409Conflict;
                    }

                    return;
                }

                var personObject = new PersonSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Id         = person.Subject,
                    Fullname   = person.Name,
                    Firstname  = person.GivenName,
                    Lastname   = person.FamilyName,
                    Email      = person.Email,
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                    Role       = signUpDoc.Role,
                };

                await personRepo.Add(personObject);
                await signUpRepo.Update(filter, update);

                await res.FromString();
            });

            // Due to a bug with adding multiple routes of the same path
            // Path in different modules, we manual auth here
            this.Get <GetUsers>("users", async(req, res) =>
            {
                var isAllowed = await PreSecurity.CheckAccessDirectly(req, res, accessRepo, date, RoleOptions.Admin);

                if (!isAllowed)
                {
                    return;
                }

                var filter = Builders <PersonSchema> .Filter
                             .Where(p => p.IsExisting == true);

                var sorter = Builders <PersonSchema> .Sort
                             .Descending(p => p.Fullname);

                var persons = await personRepo.FindAll(filter, sorter);

                var personsWithoutId = persons.Select((p) =>
                {
                    p.InternalId = null;
                    return(p);
                });

                var responceBody = new PersonsResponce
                {
                    ContentList = new List <PersonSchema>(personsWithoutId),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responceBody);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                await res.FromBson(responceBody);
            });

            this.Post <PostAccessToken>("users/{id}/tokens/", async(req, res) =>
            {
                string userID = req.RouteValues.As <string>("id");
                var userDoc   = await personRepo.FindById(userID);

                if (userDoc == null || !userDoc.IsExisting)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                var newAccessRequest = await req.BindAndValidate <AccessTokenRequest>();
                if (!newAccessRequest.ValidationResult.IsValid)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                var person = await GoogleApi.ValidateUser(newAccessRequest.Data.idToken, env);
                if (person == null)
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                if (person.Subject != userID)
                {
                    res.StatusCode = Status409Conflict;
                    return;
                }

                string newToken = Generate.GetRandomToken();
                string newHash  = PasswordHasher.Hash(newToken);

                var tokenObject = new AccessTokenSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Hash       = newHash,
                    User       = userDoc.InternalId,
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                    Role       = userDoc.Role,
                };

                await accessRepo.Add(tokenObject);

                if (newAccessRequest.Data.claimToken != null)
                {
                    var filterClaims = Builders <ClaimTokenSchema> .Filter
                                       .Where(c => c.Hash == PasswordHasher.Hash(newAccessRequest.Data.claimToken));

                    var claimDoc = await claimRepo.FindOne(filterClaims);

                    // 401 returned twice, which may be hard for the client to interpret
                    if (claimDoc == null || claimDoc.CreatedAt.IsAfter(date, claimDoc.ExpirationSeconds))
                    {
                        res.StatusCode = Status401Unauthorized;
                        return;
                    }

                    // Don't allow overwriting an access token
                    if (claimDoc.Access == null && claimDoc.IsExisting)
                    {
                        var update = Builders <ClaimTokenSchema> .Update
                                     .Set(c => c.Access, tokenObject.InternalId)
                            #pragma warning disable SA1515
                                     // Unfortunately claim access tokens have to be saved to the database
                                     // So that state can be communicated between clients
                            #pragma warning restore SA1515
                                     .Set(c => c.AccessToken, newToken);

                        await claimRepo.Update(filterClaims, update);
                    }

                    await res.FromString();
                }
                else
                {
                    var responce = new AccessTokenResponce
                    {
                        AccessToken = newToken,
                        Expiration  = TimerExtra.ProjectSeconds(date, tokenObject.ExpirationSeconds),
                    };

                    string json = JsonQuery.FulfilEncoding(req.Query, responce);
                    if (json != null)
                    {
                        await res.FromJson(json);
                        return;
                    }

                    await res.FromBson(responce.ToBsonDocument());
                }
            });

            this.Post <PostClaims>("authentication/claims/", async(req, res) =>
            {
                string newToken = Generate.GetRandomToken();
                string newHash  = PasswordHasher.Hash(newToken);

                var tokenDoc = new ClaimTokenSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Hash       = newHash,
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                };

                await claimRepo.Add(tokenDoc);

                var responce = new ClaimTokenResponce
                {
                    ClaimToken = newToken,
                    Expiration = TimerExtra.ProjectSeconds(date, tokenDoc.ExpirationSeconds),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responce);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                await res.FromBson(responce.ToBsonDocument());
            });

            this.Get <GetClaims>("authentication/claims/", async(req, res) =>
            {
                string claimToken = req.Cookies["ExperienceCapture-Claim-Token"];
                if (claimToken == null)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                var filter = Builders <ClaimTokenSchema> .Filter
                             .Where(c => c.Hash == PasswordHasher.Hash(claimToken));
                var claimDoc = await claimRepo.FindOne(filter);

                if (claimDoc == null)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                if (!claimDoc.IsExisting || claimDoc.CreatedAt.IsAfter(date, claimDoc.ExpirationSeconds))
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                if (claimDoc.Access == null)
                {
                    res.StatusCode = Status202Accepted;
                    await res.FromString("PENDING");
                    return;
                }

                var update = Builders <ClaimTokenSchema> .Update
                             .Set(c => c.IsExisting, false)
                    #pragma warning disable SA1515
                             // Removes the access token from the database
                             // Important to increase security
                    #pragma warning restore SA1515
                             .Set(c => c.AccessToken, null);

                await claimRepo.Update(filter, update);

                var responce = new AccessTokenResponce
                {
                    AccessToken = claimDoc.AccessToken,
                    Expiration  = TimerExtra.ProjectSeconds(date, claimDoc.ExpirationSeconds),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responce);
                if (json != null)
                {
                    await res.FromString(json);
                    return;
                }

                await res.FromBson(responce.ToBsonDocument());
            });

            this.Post <PostAdmin>("authentication/admins/", async(req, res) =>
            {
                var newAdmin = await req.BindAndValidate <AdminPasswordRequest>();
                if (!newAdmin.ValidationResult.IsValid)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                if (!PasswordHasher.Check(newAdmin.Data.password, env.PasswordHash) && env.SkipValidation != "true")
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                string newToken = Generate.GetRandomToken();

                var tokenDoc = new SignUpTokenSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Hash       = PasswordHasher.Hash(newToken),
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                    Role       = RoleOptions.Admin,
                };

                await signUpRepo.Add(tokenDoc);

                var responce = new SignUpTokenResponce
                {
                    SignUpToken = newToken,
                    Expiration  = TimerExtra.ProjectSeconds(date, tokenDoc.ExpirationSeconds),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responce);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                await res.FromBson(responce.ToBsonDocument());
            });
        }
        /// <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);
            });
        }
Exemplo n.º 10
0
        /// <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();
            });
        }