From b9c29bf537e3b5a64c8df7a29377e06596a7aee4 Mon Sep 17 00:00:00 2001
From: Omar Roth <omarroth@hotmail.com>
Date: Thu, 8 Nov 2018 00:12:14 -0600
Subject: [PATCH] Add option for user to delete their account

---
 src/invidious.cr                            | 76 ++++++++++++++++++++-
 src/invidious/helpers/helpers.cr            | 48 +++++++++++++
 src/invidious/views/clear_watch_history.ecr | 17 +++++
 src/invidious/views/delete_account.ecr      | 17 +++++
 src/invidious/views/preferences.ecr         |  8 ++-
 5 files changed, 161 insertions(+), 5 deletions(-)
 create mode 100644 src/invidious/views/clear_watch_history.ecr
 create mode 100644 src/invidious/views/delete_account.ecr

diff --git a/src/invidious.cr b/src/invidious.cr
index 3c251d96..7cf60531 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -896,6 +896,7 @@ post "/login" do |env|
   end
 end
 
+# TODO: Update this with using the same method for /clear_watch_history to prevent CSRF
 get "/signout" do |env|
   referer = get_referer(env)
 
@@ -910,7 +911,7 @@ get "/signout" do |env|
   end
 
   env.request.cookies.add_response_headers(env.response.headers)
-  env.redirect URI.unescape(referer)
+  env.redirect referer
 end
 
 get "/preferences" do |env|
@@ -1402,14 +1403,83 @@ get "/subscription_ajax" do |env|
   env.redirect referer
 end
 
-get "/clear_watch_history" do |env|
+get "/delete_account" do |env|
   user = env.get? "user"
-
   referer = get_referer(env)
 
   if user
     user = user.as(User)
 
+    challenge, token = create_response(user.email, "delete_account", HMAC_KEY)
+
+    templated "delete_account"
+  else
+    env.redirect referer
+  end
+end
+
+post "/delete_account" do |env|
+  user = env.get? "user"
+  referer = get_referer(env)
+
+  if user
+    user = user.as(User)
+
+    challenge = env.params.body["challenge"]?
+    token = env.params.body["token"]?
+
+    begin
+      validate_response(challenge, token, "delete_account", HMAC_KEY)
+    rescue ex
+      error_message = ex.message
+      next templated "error"
+    end
+
+    view_name = "subscriptions_#{sha256(user.email)[0..7]}"
+    PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}")
+    PG_DB.exec("DELETE FROM users * WHERE email = $1", user.email)
+
+    env.request.cookies.each do |cookie|
+      cookie.expires = Time.new(1990, 1, 1)
+    end
+    env.request.cookies.add_response_headers(env.response.headers)
+  end
+
+  env.redirect referer
+end
+
+get "/clear_watch_history" do |env|
+  user = env.get? "user"
+  referer = get_referer(env)
+
+  if user
+    user = user.as(User)
+
+    challenge, token = create_response(user.email, "clear_watch_history", HMAC_KEY)
+
+    templated "clear_watch_history"
+  else
+    env.redirect referer
+  end
+end
+
+post "/clear_watch_history" do |env|
+  user = env.get? "user"
+  referer = get_referer(env)
+
+  if user
+    user = user.as(User)
+
+    challenge = env.params.body["challenge"]?
+    token = env.params.body["token"]?
+
+    begin
+      validate_response(challenge, token, "clear_watch_history", HMAC_KEY)
+    rescue ex
+      error_message = ex.message
+      next templated "error"
+    end
+
     PG_DB.exec("UPDATE users SET watched = '{}' WHERE email = $1", user.email)
   end
 
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 92a2e1b1..65493790 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -389,3 +389,51 @@ def extract_items(nodeset, ucid = nil)
 
   return items
 end
+
+def create_response(user_id, operation, key)
+  nonce = Random::Secure.hex(4)
+  expire = Time.now + 6.hours
+
+  challenge = "#{expire.to_unix}-#{nonce}-#{user_id}-#{operation}"
+  token = OpenSSL::HMAC.digest(:sha256, key, challenge)
+
+  challenge = Base64.urlsafe_encode(challenge)
+  token = Base64.urlsafe_encode(token)
+
+  return challenge, token
+end
+
+def validate_response(challenge, token, action, key)
+  if !challenge
+    raise "Hidden field \"challenge\" is a required field"
+  end
+
+  if !token
+    raise "Hidden field \"token\" is a required field"
+  end
+
+  challenge = Base64.decode_string(challenge)
+  if challenge.split("-").size == 4
+    expire, nonce, user_id, operation = challenge.split("-")
+
+    expire = expire.to_i?
+    expire ||= 0
+  else
+    raise "Invalid challenge"
+  end
+
+  challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge)
+  challenge = Base64.urlsafe_encode(challenge)
+
+  if challenge != token
+    raise "Invalid token"
+  end
+
+  if operation != action
+    raise "Invalid token"
+  end
+
+  if expire < Time.now.to_unix
+    raise "Token is expired, please try again"
+  end
+end
diff --git a/src/invidious/views/clear_watch_history.ecr b/src/invidious/views/clear_watch_history.ecr
new file mode 100644
index 00000000..9a726a68
--- /dev/null
+++ b/src/invidious/views/clear_watch_history.ecr
@@ -0,0 +1,17 @@
+<div class="h-box">
+    <form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.escape(referer) %>" method="post">
+        <legend>Clear watch history?</legend>
+
+        <div class="pure-g">
+            <div class="pure-u-1-2">
+                <button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">Yes</button>
+            </div>
+            <div class="pure-u-1-2">
+                <a class="pure-button" href="<%= referer %>">No</a>
+            </div>
+        </div>
+
+        <input type="hidden" name="token" value="<%= token %>">
+        <input type="hidden" name="challenge" value="<%= challenge %>">
+    </form>
+</div>
diff --git a/src/invidious/views/delete_account.ecr b/src/invidious/views/delete_account.ecr
new file mode 100644
index 00000000..8f2b61d6
--- /dev/null
+++ b/src/invidious/views/delete_account.ecr
@@ -0,0 +1,17 @@
+<div class="h-box">
+    <form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.escape(referer) %>" method="post">
+        <legend>Delete account?</legend>
+
+        <div class="pure-g">
+            <div class="pure-u-1-2">
+                <button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary">Yes</button>
+            </div>
+            <div class="pure-u-1-2">
+                <a class="pure-button" href="<%= referer %>">No</a>
+            </div>
+        </div>
+
+        <input type="hidden" name="token" value="<%= token %>">
+        <input type="hidden" name="challenge" value="<%= challenge %>">
+    </form>
+</div>
diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr
index be15a54d..72b1d609 100644
--- a/src/invidious/views/preferences.ecr
+++ b/src/invidious/views/preferences.ecr
@@ -150,17 +150,21 @@ function update_value(element) {
             <legend>Data preferences</legend>
 
             <div class="pure-control-group">
-                <a href="/clear_watch_history?referer=<%= referer %>">Clear watch history</a>
+                <a href="/clear_watch_history?referer=<%= URI.escape(referer) %>">Clear watch history</a>
             </div>
             
             <div class="pure-control-group">
-                <a href="/data_control?referer=<%= referer %>">Import/Export data</a>
+                <a href="/data_control?referer=<%= URI.escape(referer) %>">Import/Export data</a>
             </div>
 
             <div class="pure-control-group">
                 <a href="/subscription_manager">Manage subscriptions</a>
             </div>
 
+            <div class="pure-control-group">
+                <a href="/delete_account?referer=<%= URI.escape(referer) %>">Delete account</a>
+            </div>
+
             <div class="pure-controls">
                 <button type="submit" class="pure-button pure-button-primary">Save preferences</button>
             </div>