public async Task <IActionResult> Post()
        {
            var account = await GetCurrentAccountAsync();

            var auth0userId = account.Auth0UserId;

            // User tracking.
            await UserEventsService.RecordEvent(new UserEvent
            {
                Account  = account,
                Category = "account",
                Name     = "delete"
            });

            // Delete the user from Auth0.
            // Of all the steps in this process, this one is most likely to fail,
            // so we do it first.
            await _auth0Client.DeleteUser(auth0userId);

            // Mark the DB data for deletion.
            account.IsDeleted   = true;
            account.Auth0UserId = "";

            Db.Accounts.Update(account);
            await Db.SaveChangesAsync();

            return(Ok());
        }
예제 #2
0
 public UserEventsServiceTest()
 {
     tests             = new AfterAndBeforeTests();
     userService       = new UserService();
     authService       = new AuthService();
     constants         = new TestConstants();
     eventService      = new EventService();
     userEventsService = new UserEventsService();
 }
예제 #3
0
        public async Task <IActionResult> Delete(Guid id)
        {
            // Validate that the user is logged in.
            var account = await GetCurrentAccountAsync();

            if (account == null)
            {
                return(Unauthorized());
            }

            // Check if the ActivityGroup exists.
            var activityGroup = await Db.ActivityGroups.FirstOrDefaultAsync(a => a.Id == id);

            if (activityGroup == null)
            {
                return(NotFound());
            }

            // Check if it belongs to the user.
            var pollRequest = Db.Polls.FirstOrDefaultAsync(p => p.Id == activityGroup.PollId);
            var poll        = await pollRequest;

            if (poll.AccountId != account.Id)
            {
                return(NotFound());
            }

            // User tracking.
            await UserEventsService.RecordEvent(new UserEvent
            {
                Account  = Account,
                Category = "activity_group",
                Name     = "delete"
            });

            // Unlink the children.
            var children = await Db.ActivityGroups.Where(a => a.PollId == poll.Id && a.ParentId == activityGroup.Id).ToListAsync();

            foreach (var child in children)
            {
                child.ParentId = null;
            }

            await Db.SaveChangesAsync();

            // Delete the group.
            Db.ActivityGroups.Remove(activityGroup);
            await Db.SaveChangesAsync();

            return(NoContent());
        }
예제 #4
0
        public async Task Delete()
        {
            var account = await GetLoggedInAccountAsync();

            var auth0userId = account.Auth0UserId;

            // User tracking.
            await UserEventsService.RecordEvent(new UserEvent
            {
                Account  = account,
                Category = "account",
                Name     = "delete"
            });

            // Delete the user from Auth0.
            // Of all the steps in this process, this one is most likely to fail,
            // so we do it first.
            await _auth0Client.DeleteUser(auth0userId);

            // Cancel the Paddle subscription.
            var paddleSubscriptionId = Account.PaddleSubscriptionId;

            if (paddleSubscriptionId != null)
            {
                try
                {
                    await _paddleClient.CancelSubscription(paddleSubscriptionId.Value);

                    Account.PaddleSubscriptionId = null;
                }
                catch (Exception)
                {
                    _log.LogError($"Could not cancel Paddle subscription {paddleSubscriptionId} " +
                                  $"for account {Account.Id}");
                }
            }

            // Mark the DB data for deletion.
            account.IsDeleted   = true;
            account.Auth0UserId = "";

            Db.Accounts.Update(account);
            await Db.SaveChangesAsync();
        }
예제 #5
0
        public async Task <IActionResult> GetCsv([FromQuery] DateTime fromTime, [FromQuery] DateTime toTime)
        {
            var poll = await GetDefaultPollAsync();

            var entries = await _timeLogService.Get(Db, poll.Id, fromTime, toTime);

            var csvRecords = entries.Select(logEntry => new CsvRecord
            {
                FromTime = logEntry.FromTime,
                ToTime   = logEntry.GetToTime(),
                Length   = logEntry.TimeBlockLength,
                Activity = logEntry.EntryText
            });

            var memoryStream = new MemoryStream();

            using (var writer = new StreamWriter(memoryStream))
            {
                using (var csv = new CsvWriter(writer))
                {
                    csv.Configuration.HasHeaderRecord = true;
                    csv.WriteRecords(csvRecords);
                    writer.Flush();
                }
            }

            // User event tracking.
            await UserEventsService.RecordEvent(new UserEvent
            {
                Account  = await GetCurrentAccountAsync(),
                Category = "daily_logs",
                Name     = "download_csv"
            });

            var body = memoryStream.ToArray();

            return(File(body, "text/csv", "history.csv"));
        }
예제 #6
0
        // This method gets called by the runtime. Use this method to add services
        // to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Basic config
            services.AddSingleton(Configuration);
            services.AddOptions();

            var backendRoutes = new BackendRoutes();

            Configuration.GetSection("BackendRoutes").Bind(backendRoutes);
            services.AddSingleton(backendRoutes);

            var stackdriverOptions = new StackdriverOptions();

            Configuration.Bind("Stackdriver", stackdriverOptions);
            services.AddSingleton(stackdriverOptions);

            // Set up the shared DataProtection keystorage when running on Google Cloud.
            if (!Environment.IsDevelopment())
            {
                services.AddDataProtection()
                // Store keys in Cloud Storage so that multiple instances
                // of the web application see the same keys.
                .PersistKeysToGoogleCloudStorage(
                    Configuration["DataProtection:Bucket"],
                    Configuration["DataProtection:Object"])
                // Protect the keys with Google KMS for encryption and fine-
                // grained access control.
                .ProtectKeysWithGoogleKms(
                    Configuration["DataProtection:KmsKeyName"]);
            }

            // Set up user event tracking.
            IUserEventsService userEventsService;

            if (!Environment.IsDevelopment())
            {
                userEventsService = new UserEventsService(stackdriverOptions);
            }
            else
            {
                userEventsService = new NullUserEventsService();
            }

            services.AddSingleton(userEventsService);

            // App services
            var restClient = new RestClient();

            services.AddSingleton <IRestClient>(restClient);

            var timerFactory = new SystemTimerFactory(new NullLogger()); // Use NullLogger until we get problems.

            services.AddSingleton <ITimerFactory>(timerFactory);

            var timeService = new SystemTimeService();

            services.AddSingleton <ITimeService>(timeService);

            var tokenService = new Auth0TokenService(restClient,
                                                     timerFactory,
                                                     LoggerFactory.CreateLogger <Auth0TokenService>());
            var auth0Client = new Auth0Client(tokenService, restClient);

            services.AddSingleton <IAuth0Client>(auth0Client);

            _accountService = new AccountService(userEventsService, LoggerFactory, timeService);
            services.AddSingleton(_accountService);

            var timeLogServie = new TimeLogService(timeService, userEventsService);

            services.AddSingleton <ITimeLogService>(timeLogServie);

            // Paddle config.
            var paddleClient = new PaddleClient(Configuration["Paddle:VendorId"],
                                                Configuration["Paddle:VendorAuthCode"],
                                                restClient,
                                                LoggerFactory);

            services.AddSingleton <IPaddleClient>(paddleClient);

            services.AddSingleton <IPaddleWebhookSignatureVerifier>(new PaddleWebhookSignatureVerifier());

            // Configure Google App Engine logging
            if (!Environment.IsDevelopment())
            {
                services.Configure <StackdriverOptions>(Configuration.GetSection("Stackdriver"));

                services.AddGoogleExceptionLogging(options =>
                {
                    options.ProjectId   = stackdriverOptions.ProjectId;
                    options.ServiceName = stackdriverOptions.ServiceName;
                    options.Version     = stackdriverOptions.Version;
                });

                services.AddGoogleTrace(options =>
                {
                    options.ProjectId = stackdriverOptions.ProjectId;
                    options.Options   = TraceOptions.Create(bufferOptions: BufferOptions.NoBuffer());
                });
            }

            services.AddEntityFrameworkNpgsql()
            .AddDbContext <MainDbContext>()
            .BuildServiceProvider();

            // ======= Authentication config =======
            services.Configure <CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded    = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme       = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme    = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.Authority = "https://maesure.auth0.com/";
                options.Audience  = "https://maesure.com/api/";
            })
            .AddCookie(options =>
            {
                options.LoginPath          = "/api/auth/login";
                options.LogoutPath         = "/api/auth/logout";
                options.SlidingExpiration  = true;
                options.ExpireTimeSpan     = TimeSpan.FromDays(90);
                options.Cookie.Expiration  = TimeSpan.FromDays(90);
                options.Cookie.SameSite    = SameSiteMode.Lax; // OAuth login will not work with "strict"
                options.Cookie.IsEssential = true;
            })
            .AddOpenIdConnect("Auth0", options =>
            {
                // Set the authority to your Auth0 domain
                options.Authority = $"https://{Configuration["Auth0:Domain"]}";

                // Configure the Auth0 Client ID and Client Secret
                options.ClientId     = Configuration["Auth0:ClientId"];
                options.ClientSecret = Configuration["Auth0:ClientSecret"];

                // Set response type to code
                options.ResponseType = "code";

                // Configure the scope
                options.Scope.Clear();
                options.Scope.Add("openid email profile");

                // Set the callback path, so Auth0 will call back to http://localhost:5000/callback
                // Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard

                // WARNING: here, "callback" is not some placeholder URL. ASP.NET expects the user to be
                // sent litteral "/callback" URL. Do not change this.
                options.CallbackPath = new PathString("/callback");

                // Configure the Claims Issuer to be Auth0
                options.ClaimsIssuer = "Auth0";

                options.Events = new OpenIdConnectEvents
                {
                    // handle the logout redirection
                    OnRedirectToIdentityProviderForSignOut = (context) =>
                    {
                        var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";

                        var postLogoutUri = context.Properties.RedirectUri;
                        if (!string.IsNullOrEmpty(postLogoutUri))
                        {
                            if (postLogoutUri.StartsWith("/"))
                            {
                                // transform to absolute
                                var request   = context.Request;
                                postLogoutUri = request.Scheme + "://" + request.Host + postLogoutUri;
                            }
                            logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
                        }

                        context.Response.Redirect(logoutUri);
                        context.HandleResponse();

                        return(Task.CompletedTask);
                    },
                    OnRedirectToIdentityProvider = (context) =>
                    {
                        // Check if we need to tell Auth0 explicitly which
                        // connection to use.
                        var properties = context.Properties;
                        var connection = properties.GetString("connection");

                        if (connection != null)
                        {
                            context.ProtocolMessage.SetParameter("connection", connection);
                        }

                        return(Task.CompletedTask);
                    },
                    OnTokenValidated = async(context) =>
                    {
                        // Ensure that the user exists in our database.
                        using (var db = new MainDbContext())
                        {
                            // Get the Auth0 user details.
                            var userClaims = context.SecurityToken.Claims;
                            var auth0Id    = userClaims.FirstOrDefault(c => c.Type == "sub").Value;
                            _log.LogInformation($"Ensuring account exists for '{auth0Id}'");

                            // See if there's a temp account session here.
                            var cookies = context.HttpContext.Request.Cookies;
                            cookies.TryGetValue(PublicWebProxyController.VisitorSessionKey, out var sessionId);

                            await _accountService.EnsureAccountEsists(db, auth0Id, sessionId);
                            _log.LogInformation($"Finished login for '{auth0Id}'");
                        }
                    },
                };
예제 #7
0
 public EventsController(UserEventsService userEventsService, ClientEventsService clientEventsService, IWebHostEnvironment environment)
 {
     _userEventsService   = userEventsService;
     _clientEventsService = clientEventsService;
     _environment         = environment;
 }
예제 #8
0
        public async Task <IActionResult> Create([FromBody] Messages.ActivityGroupCreateRequest request)
        {
            // Validate that the user is logged in.
            var account = await GetCurrentAccountAsync();

            if (account == null)
            {
                return(Unauthorized());
            }

            // Basic validation.
            var nameError = ValidateName(request.Name);

            if (nameError != null)
            {
                return(nameError);
            }

            var parentError = ValidateParentDescription(request.ParentId,
                                                        request.ParentMatchResponseText,
                                                        request.GrandparentId,
                                                        "parent");

            if (parentError != null)
            {
                return(parentError);
            }

            // Data validation.
            var poll = await GetDefaultPollAsync();

            var parentExistsError = await ValidateGroupExists(poll,
                                                              request.ParentId,
                                                              request.ParentMatchResponseText,
                                                              request.GrandparentId,
                                                              "parent");

            if (parentExistsError != null)
            {
                return(parentExistsError);
            }

            // User tracking.
            await UserEventsService.RecordEvent(new UserEvent
            {
                Account  = Account,
                Category = "activity_group",
                Name     = "create"
            });

            // Create.
            var newGroup = new ActivityGroup
            {
                Id       = Guid.NewGuid(),
                ParentId = request.ParentId,
                Name     = request.Name,
                PollId   = poll.Id,
                Position = 0,
            };

            if (!string.IsNullOrWhiteSpace(request.ParentMatchResponseText))
            {
                var newParent = new ActivityGroup
                {
                    Id                = Guid.NewGuid(),
                    ParentId          = request.GrandparentId,
                    Name              = request.ParentMatchResponseText,
                    MatchResponseText = request.ParentMatchResponseText,
                    Position          = 0,
                    PollId            = poll.Id
                };

                newGroup.ParentId = newParent.Id;
                Db.ActivityGroups.Add(newParent);
            }

            Db.ActivityGroups.Add(newGroup);
            await Db.SaveChangesAsync();

            return(NoContent());
        }
예제 #9
0
        public async Task <IActionResult> Move([FromBody] Messages.ActivityGroupMoveRequest request)
        {
            // Validate that the user is logged in.
            var account = await GetCurrentAccountAsync();

            if (account == null)
            {
                return(Unauthorized());
            }

            // Basic validation.
            if (request.Id != null && !string.IsNullOrWhiteSpace(request.MatchResponseText))
            {
                return(BadRequest("Should not provide both 'id' and 'matchResponseText'"));
            }
            else if (request.Id == null && string.IsNullOrWhiteSpace(request.MatchResponseText))
            {
                return(BadRequest("Must provide either 'id' or 'matchResponseText'"));
            }

            var nameError = ValidateLength(request.MatchResponseText, ActivityGroup.MaxMatchResponseTextLength, "matchResponseText");

            if (nameError != null)
            {
                return(nameError);
            }

            var parentError = ValidateParentDescription(request.TargetParentId,
                                                        request.TargetParentMatchResponseText,
                                                        request.TargetGrandparentId,
                                                        "targetParent");

            if (parentError != null)
            {
                return(parentError);
            }

            if (request.TargetIsUncategorized != null &&
                (request.TargetParentId != null || !string.IsNullOrWhiteSpace(request.TargetParentMatchResponseText)))
            {
                return(BadRequest("Cannot provide 'targetIsUncategorized' when providing target parent details."));
            }

            if (!string.IsNullOrWhiteSpace(request.MatchResponseText) &&
                request.MatchResponseText == request.TargetParentMatchResponseText)
            {
                return(BadRequest("'matchResponseText' and 'targetParentMatchResponseText' cannot be the same. " +
                                  "Cannot make an activity group its own parent."));
            }

            // Data validation.
            Poll          poll          = null;
            ActivityGroup activityGroup = null;

            if (request.Id != null)
            {
                // This should be an actual activity group. Check if it exists.
                activityGroup = await Db.ActivityGroups.FirstOrDefaultAsync(g => g.Id == request.Id);

                if (activityGroup == null)
                {
                    return(NotFound($"Could not find ActivityGroup with id = {request.Id}"));
                }

                var pollId = activityGroup.PollId;
                poll = await Db.Polls.FirstOrDefaultAsync(p => p.Id == pollId);

                if (poll == null)
                {
                    throw new Exception($"Could not find a poll wit Id {pollId}");
                }
                else if (poll.AccountId != account.Id)
                {
                    return(NotFound($"Could not find ActivityGroup with id = {request.Id}"));
                }
            }
            else
            {
                // Confirm that this ActivityGroup does not exist.
                poll = await GetDefaultPollAsync();

                var existingGroup = await Db.ActivityGroups.FirstOrDefaultAsync(g => g.PollId == poll.Id &&
                                                                                g.MatchResponseText == request.MatchResponseText);

                if (existingGroup != null)
                {
                    return(BadRequest("There is already an ActivityGroup with matchResponseText = " +
                                      $"'{request.MatchResponseText}'. Its id is {existingGroup.Id}. Please target it by its id."));
                }

                // Create the activity group.
                activityGroup = new ActivityGroup
                {
                    Id   = Guid.NewGuid(),
                    Name = request.MatchResponseText,
                    MatchResponseText = request.MatchResponseText,
                    PollId            = poll.Id,
                    Position          = 0
                };

                Db.ActivityGroups.Add(activityGroup);
            }

            var parentExistsError = await ValidateGroupExists(poll,
                                                              request.TargetParentId,
                                                              request.TargetParentMatchResponseText,
                                                              request.TargetGrandparentId,
                                                              "target parent");

            if (parentExistsError != null)
            {
                return(parentExistsError);
            }

            // Make sure that this ActivityGroup will not be its own ancestor.
            var nextAncestorId = request.TargetParentId;

            while (nextAncestorId.HasValue)
            {
                var ancestor = await Db.ActivityGroups.FirstAsync(g => g.PollId == poll.Id && g.Id == nextAncestorId.Value);

                if (ancestor == null)
                {
                    break;
                }

                if (ancestor.Id == request.Id)
                {
                    return(BadRequest("Cannot make make ActivityGroup its own ancestor."));
                }

                nextAncestorId = ancestor.ParentId;
            }

            // User tracking.
            await UserEventsService.RecordEvent(new UserEvent
            {
                Account  = Account,
                Category = "activity_group",
                Name     = "move"
            });

            // Perform the move.
            if (request.TargetParentId == null && string.IsNullOrEmpty(request.TargetParentMatchResponseText))
            {
                // Move to the top level.
                activityGroup.ParentId = null;
            }
            else if (request.TargetParentId != null)
            {
                // Move it under the parent.
                activityGroup.ParentId = request.TargetParentId;
            }
            else if (!string.IsNullOrEmpty(request.TargetParentMatchResponseText))
            {
                // Create the target parent.
                var parent = new ActivityGroup
                {
                    Id   = Guid.NewGuid(),
                    Name = request.TargetParentMatchResponseText,
                    MatchResponseText = request.TargetParentMatchResponseText,
                    PollId            = poll.Id,
                    Position          = 0
                };

                if (request.TargetGrandparentId != null)
                {
                    parent.ParentId = request.TargetGrandparentId;
                }

                Db.ActivityGroups.Add(parent);
                activityGroup.ParentId = parent.Id;
            }
            else
            {
                // We should not enter here.
                throw new Exception("Reached unreacheable code in " + nameof(ActivityGroupsController) + ".Move().");
            }

            await Db.SaveChangesAsync();

            return(NoContent());
        }
예제 #10
0
        public async Task <IActionResult> CreateTempAccount()
        {
            // 1. Check whether the account exists
            // 2. Create account
            // 3. Create the poll
            // 4. Return

            // Check whether the user is already logged in.
            if (IsUserLoggedIn())
            {
                throw new Exception("Cannot create a temporary account because the user already has a permanent account.");
            }

            // Check if the account exists.
            var found = Request.Cookies.TryGetValue(PublicWebProxyController.VisitorSessionKey, out var sessionId);

            if (!found)
            {
                throw new Exception($"Could not find cookie '{PublicWebProxyController.VisitorSessionKey}'");
            }

            var existingAccount =
                await Db.Accounts.FirstOrDefaultAsync(a => a.TempAccountSessionId == sessionId && a.IsDeleted == false);

            if (existingAccount != null)
            {
                return(BadRequest($"Account for sessionId={sessionId} already exists"));
            }

            // Create the Account object in DB.
            // On error we just crash for now.
            var newAccount = new Account
            {
                Id                   = Guid.NewGuid(),
                Name                 = "User",
                Auth0UserId          = "",
                TempAccountSessionId = sessionId
            };

            Db.Accounts.Add(newAccount);
            await Db.SaveChangesAsync();

            // Create a poll for the user.
            var poll = new Poll
            {
                Id               = Guid.NewGuid(),
                AccountId        = newAccount.Id,
                Name             = "User poll",
                ActiveFrom       = TimeSpan.FromHours(0),
                ActiveTo         = TimeSpan.FromHours(24),
                IsActive         = true,
                DesiredFrequency = GlobalSettings.StartingFrequencyMin,
                PollType         = Messages.PollType.OpenText,
                WasStarted       = true,
                StartedAt        = DateTime.UtcNow
            };

            Db.Polls.Add(poll);
            await Db.SaveChangesAsync();

            // Prepare the final result.
            var msgPoll = new Messages.PollMsg
            {
                Id               = poll.Id,
                IsActive         = poll.IsActive,
                DesiredFrequency = poll.DesiredFrequency,
                WasStarted       = poll.WasStarted,
                StartedAt        = poll.StartedAt
            };

            var result = new Messages.CreateTempAccountResult
            {
                User = new Messages.UserReply
                {
                    AccountType = Messages.AccountType.Temporary,
                },
                DefaultPoll = msgPoll
            };

            // User event tracking.
            await UserEventsService.RecordEvent(new UserEvent
            {
                Account  = newAccount,
                Category = "temp_account",
                Name     = "create"
            });

            return(Ok(result));
        }