Przeglądaj źródła

feat: add delete account section to settings

poeti8 6 lat temu
rodzic
commit
ed4d528c8d

+ 72 - 0
client/components/Settings/SettingsDeleteAccount.tsx

@@ -0,0 +1,72 @@
+import { useFormState } from "react-use-form-state";
+import React, { FC, useState } from "react";
+import Router from "next/router";
+import axios from "axios";
+
+import { getAxiosConfig } from "../../utils";
+import { Col, RowCenterV } from "../Layout";
+import Text, { H2, Span } from "../Text";
+import { useMessage } from "../../hooks";
+import { TextInput } from "../Input";
+import { APIv2 } from "../../consts";
+import { Button } from "../Button";
+import Icon from "../Icon";
+
+const SettingsDeleteAccount: FC = () => {
+  const [message, setMessage] = useMessage(1500);
+  const [loading, setLoading] = useState(false);
+  const [formState, { password, label }] = useFormState(null, {
+    withIds: true
+  });
+
+  const onSubmit = async e => {
+    e.preventDefault();
+    if (loading) return;
+    setLoading(true);
+    try {
+      await axios.post(
+        `${APIv2.Users}/delete`,
+        formState.values,
+        getAxiosConfig()
+      );
+      Router.push("/logout");
+    } catch (error) {
+      setMessage(error.response.data.error);
+    }
+    setLoading(false);
+  };
+
+  return (
+    <Col alignItems="flex-start" maxWidth="100%">
+      <H2 mb={4} bold>
+        Delete account
+      </H2>
+      <Text mb={4}>
+        Delete your account from {process.env.SITE_NAME}. All of your data
+        including your <Span bold>LINKS</Span> and <Span bold>STATS</Span> will
+        be deleted.
+      </Text>
+      <Text
+        {...label("password")}
+        as="label"
+        mb={[2, 3]}
+        fontSize={[15, 16]}
+        bold
+      >
+        Password
+      </Text>
+      <RowCenterV as="form" onSubmit={onSubmit}>
+        <TextInput placeholder="Password..." {...password("password")} mr={3} />
+        <Button color="red" type="submit" disabled={loading}>
+          <Icon name={loading ? "spinner" : "trash"} mr={2} stroke="white" />
+          {loading ? "Deleting..." : "Delete"}
+        </Button>
+      </RowCenterV>
+      <Text fontSize={15} mt={3} color={message.color}>
+        {message.text}
+      </Text>
+    </Col>
+  );
+};
+
+export default SettingsDeleteAccount;

+ 3 - 0
client/pages/settings.tsx

@@ -1,6 +1,7 @@
 import { NextPage } from "next";
 import React from "react";
 
+import SettingsDeleteAccount from "../components/Settings/SettingsDeleteAccount";
 import SettingsPassword from "../components/Settings/SettingsPassword";
 import SettingsDomain from "../components/Settings/SettingsDomain";
 import SettingsApi from "../components/Settings/SettingsApi";
@@ -30,6 +31,8 @@ const SettingsPage: NextPage = () => {
         <SettingsPassword />
         <Divider mt={4} mb={48} />
         <SettingsApi />
+        <Divider mt={4} mb={48} />
+        <SettingsDeleteAccount />
       </Col>
       <Footer />
     </AppWrapper>

+ 5 - 0
server/handlers/users.ts

@@ -12,3 +12,8 @@ export const get = async (req, res) => {
 
   return res.status(200).send(data);
 };
+
+export const remove = async (req, res) => {
+  await query.user.remove(req.user);
+  return res.status(200).send("OK");
+};

+ 15 - 4
server/handlers/validators.ts

@@ -2,6 +2,7 @@ import { body, param } from "express-validator";
 import { isAfter, subDays, subHours } from "date-fns";
 import urlRegex from "url-regex";
 import { promisify } from "util";
+import bcrypt from "bcryptjs";
 import axios from "axios";
 import dns from "dns";
 import URL from "url";
@@ -94,7 +95,7 @@ export const createLink = [
       });
       req.body.domain = domain || null;
 
-      return !!domain;
+      if (!domain) return Promise.reject();
     })
     .withMessage("You can't use this domain.")
 ];
@@ -125,12 +126,12 @@ export const addDomain = [
     .withMessage("You can't use the default domain.")
     .custom(async (value, { req }) => {
       const domains = await query.domain.get({ user_id: req.user.id });
-      return domains.length === 0;
+      if (domains.length !== 0) return Promise.reject();
     })
     .withMessage("You already own a domain. Contact support if you need more.")
     .custom(async value => {
       const domain = await query.domain.find({ address: value });
-      return !domain || !domain.user_id || !domain.banned;
+      if (domain?.user_id || domain?.banned) return Promise.reject();
     })
     .withMessage("You can't add this domain."),
   body("homepage")
@@ -225,7 +226,7 @@ export const signup = [
         req.user = user;
       }
 
-      return !user || !user.verified;
+      if (user?.verified) return Promise.reject();
     })
     .withMessage("You can't use this email address.")
 ];
@@ -259,6 +260,16 @@ export const resetPasswordRequest = [
     .withMessage("Email length must be max 255.")
 ];
 
+export const deleteUser = [
+  body("password", "Password is not valid.")
+    .exists({ checkFalsy: true, checkNull: true })
+    .isLength({ min: 8, max: 64 })
+    .custom(async (password, { req }) => {
+      const isMatch = await bcrypt.compare(password, req.user.password);
+      if (!isMatch) return Promise.reject();
+    })
+];
+
 export const cooldown = (user: User) => {
   if (!env.GOOGLE_SAFE_BROWSING_KEY || !user || !user.cooldowns) return;
 

+ 1 - 11
server/models/domain.ts

@@ -23,19 +23,9 @@ export async function createDomainTable(knex: Knex) {
         .integer("user_id")
         .references("id")
         .inTable("users")
+        .onDelete("SET NULL")
         .unique();
       table.timestamps(false, true);
     });
   }
-
-  const hasUUID = await knex.schema.hasColumn("domains", "uuid");
-  if (!hasUUID) {
-    await knex.schema.raw('create extension if not exists "uuid-ossp"');
-    await knex.schema.alterTable("domains", table => {
-      table
-        .uuid("uuid")
-        .notNullable()
-        .defaultTo(knex.raw("uuid_generate_v4()"));
-    });
-  }
 }

+ 0 - 3
server/server.ts

@@ -12,7 +12,6 @@ import Raven from "raven";
 import * as helpers from "./handlers/helpers";
 import * as links from "./handlers/links";
 import * as auth from "./handlers/auth";
-import { initializeDb } from "./knex";
 import __v1Routes from "./__v1";
 import routes from "./routes";
 
@@ -28,8 +27,6 @@ const app = nextApp({ dir: "./client", dev: env.isDev });
 const handle = app.getRequestHandler();
 
 app.prepare().then(async () => {
-  await initializeDb();
-
   const server = express();
   server.set("trust proxy", true);