Ver Fonte

Merge pull request #139 from thedevs-network/feature/homepage-redirection

Feature/homepage redirection
Pouria Ezzati há 7 anos atrás
pai
commit
4f10803caa

+ 6 - 2
client/actions/__test__/auth.js

@@ -86,7 +86,9 @@ describe('auth actions', () => {
         },
         {
           type: SET_DOMAIN,
-          payload: ''
+          payload: {
+            customDomain: '',
+          }
         },
         { type: SHOW_PAGE_LOADING }
       ];
@@ -151,7 +153,9 @@ describe('auth actions', () => {
         },
         {
           type: SET_DOMAIN,
-          payload: ''
+          payload: {
+            customDomain: '',
+          }
         }
       ];
 

+ 16 - 5
client/actions/__test__/settings.js

@@ -41,6 +41,7 @@ describe('settings actions', () => {
     it('should dispatch SET_APIKEY and SET_DOMAIN when getting user settings have been done', done => {
       const apikey = '123';
       const customDomain = 'test.com';
+      const homepage = '';
 
       nock('http://localhost', {
         reqheaders: {
@@ -48,14 +49,17 @@ describe('settings actions', () => {
         }
       })
         .get('/api/auth/usersettings')
-        .reply(200, { apikey, customDomain });
+        .reply(200, { apikey, customDomain, homepage });
 
       const store = mockStore({});
 
       const expectedActions = [
         {
           type: SET_DOMAIN,
-          payload: customDomain
+          payload: {
+            customDomain,
+            homepage: ''
+          }
         },
         {
           type: SET_APIKEY,
@@ -76,6 +80,7 @@ describe('settings actions', () => {
   describe('#setCustomDomain()', () => {
     it('should dispatch SET_DOMAIN when setting custom domain has been done', done => {
       const customDomain = 'test.com';
+      const homepage = '';
 
       nock('http://localhost', {
         reqheaders: {
@@ -83,7 +88,7 @@ describe('settings actions', () => {
         }
       })
         .post('/api/url/customdomain')
-        .reply(200, { customDomain });
+        .reply(200, { customDomain, homepage });
 
       const store = mockStore({});
 
@@ -91,12 +96,18 @@ describe('settings actions', () => {
         { type: DOMAIN_LOADING },
         {
           type: SET_DOMAIN,
-          payload: customDomain
+          payload: {
+            customDomain,
+            homepage: ''
+          }
         }
       ];
 
       store
-        .dispatch(setCustomDomain(customDomain))
+        .dispatch(setCustomDomain({
+          customDomain,
+          homepage: ''
+        }))
         .then(() => {
           expect(store.getActions()).to.deep.equal(expectedActions);
           done();

+ 2 - 2
client/actions/auth.js

@@ -47,7 +47,7 @@ export const loginUser = payload => async dispatch => {
     cookie.set('token', token, { expires: 7 });
     dispatch(authRenew());
     dispatch(authUser(decodeJwt(token)));
-    dispatch(setDomain(decodeJwt(token).domain));
+    dispatch(setDomain({ customDomain: decodeJwt(token).domain }));
     dispatch(showPageLoading());
     Router.push('/');
   } catch ({ response }) {
@@ -78,7 +78,7 @@ export const renewAuthUser = () => async (dispatch, getState) => {
     cookie.set('token', token, { expires: 7 });
     dispatch(authRenew());
     dispatch(authUser(decodeJwt(token)));
-    dispatch(setDomain(decodeJwt(token).domain));
+    dispatch(setDomain({ customDomain: decodeJwt(token).domain }));
   } catch (error) {
     cookie.remove('token');
     dispatch(unauthUser());

+ 5 - 5
client/actions/settings.js

@@ -23,11 +23,11 @@ export const showDomainInput = () => ({ type: SHOW_DOMAIN_INPUT });
 
 export const getUserSettings = () => async dispatch => {
   try {
-    const { data } = await axios.get('/api/auth/usersettings', {
+    const { data: { apikey, customDomain, homepage } } = await axios.get('/api/auth/usersettings', {
       headers: { Authorization: cookie.get('token') },
     });
-    dispatch(setDomain(data.customDomain));
-    dispatch(setApiKey(data.apikey));
+    dispatch(setDomain({ customDomain, homepage }));
+    dispatch(setApiKey(apikey));
   } catch (error) {
     //
   }
@@ -36,10 +36,10 @@ export const getUserSettings = () => async dispatch => {
 export const setCustomDomain = params => async dispatch => {
   dispatch(showDomainLoading());
   try {
-    const { data } = await axios.post('/api/url/customdomain', params, {
+    const { data: { customDomain, homepage } } = await axios.post('/api/url/customdomain', params, {
       headers: { Authorization: cookie.get('token') },
     });
-    dispatch(setDomain(data.customDomain));
+    dispatch(setDomain({ customDomain, homepage }));
   } catch ({ response }) {
     dispatch(setDomainError(response.data.error));
   }

+ 2 - 1
client/components/Settings/Settings.js

@@ -163,7 +163,8 @@ class Settings extends Component {
     e.preventDefault();
     if (this.props.domainLoading) return null;
     const customDomain = e.currentTarget.elements.customdomain.value;
-    return this.props.setCustomDomain({ customDomain });
+    const homepage = e.currentTarget.elements.homepage.value;
+    return this.props.setCustomDomain({ customDomain, homepage });
   }
 
   deleteDomain() {

+ 85 - 22
client/components/Settings/SettingsDomain.js

@@ -9,6 +9,9 @@ import { fadeIn } from '../../helpers/animations';
 const Form = styled.form`
   position: relative;
   display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  justify-content: flex-start;
   margin: 32px 0;
   animation: ${fadeIn} 0.8s ease;
 
@@ -25,6 +28,11 @@ const Form = styled.form`
 const DomainWrapper = styled.div`
   display: flex;
   align-items: center;
+`;
+
+const ButtonWrapper = styled.div`
+  display: flex;
+  align-items: center;
   margin: 32px 0;
   animation: ${fadeIn} 0.8s ease;
 
@@ -42,11 +50,40 @@ const DomainWrapper = styled.div`
   }
 `;
 
-const Domain = styled.span`
-  margin-right: 16px;
+const Domain = styled.h4`
+  margin: 0 16px 0 0;
   font-size: 20px;
   font-weight: bold;
-  border-bottom: 2px dotted #999;
+
+  span {
+    border-bottom: 2px dotted #999;
+  }
+`;
+
+const Homepage = styled.h6`
+  margin: 0 16px 0 0;
+  font-size: 14px;
+  font-weight: 300;
+
+  span {
+    border-bottom: 2px dotted #999;
+  }
+`;
+
+const InputWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+`;
+
+const LabelWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+
+  span {
+    font-weight: bold;
+    margin-bottom: 8px;
+  }
 `;
 
 const SettingsDomain = ({ settings, handleCustomDomain, loading, showDomainInput, showModal }) => (
@@ -60,27 +97,53 @@ const SettingsDomain = ({ settings, handleCustomDomain, loading, showDomainInput
       Point your domain A record to <b>164.132.153.221</b> then add the domain via form below:
     </p>
     {settings.customDomain && !settings.domainInput ? (
-      <DomainWrapper>
-        <Domain>{settings.customDomain}</Domain>
-        <Button icon="edit" onClick={showDomainInput}>
-          Change
-        </Button>
-        <Button color="gray" icon="x" onClick={showModal}>
-          Delete
-        </Button>
-      </DomainWrapper>
+      <div>
+        <DomainWrapper>
+          <Domain>
+            <span>{settings.customDomain}</span>
+          </Domain>
+          <Homepage>
+            (Homepage redirects to <span>{settings.homepage || window.location.hostname}</span>)
+          </Homepage>
+        </DomainWrapper>
+        <ButtonWrapper>
+          <Button icon="edit" onClick={showDomainInput}>
+            Change
+          </Button>
+          <Button color="gray" icon="x" onClick={showModal}>
+            Delete
+          </Button>
+        </ButtonWrapper>
+      </div>
     ) : (
       <Form onSubmit={handleCustomDomain}>
-        <Error type="domain" left={0} bottom={-48} />
-        <TextInput
-          id="customdomain"
-          name="customdomain"
-          type="text"
-          placeholder="example.com"
-          defaultValue={settings.customDomain}
-          height={44}
-          small
-        />
+        <Error type="domain" left={0} bottom={-54} />
+        <InputWrapper>
+          <LabelWrapper htmlFor="customdomain">
+            <span>Domain</span>
+            <TextInput
+              id="customdomain"
+              name="customdomain"
+              type="text"
+              placeholder="example.com"
+              defaultValue={settings.customDomain}
+              height={44}
+              small
+            />
+          </LabelWrapper>
+          <LabelWrapper>
+            <span>Homepage (Optional)</span>
+            <TextInput
+              id="homepage"
+              name="homepage"
+              type="text"
+              placeholder="Homepage URL"
+              defaultValue={settings.homepage}
+              height={44}
+              small
+            />
+          </LabelWrapper>
+        </InputWrapper>
         <Button type="submit" color="purple" icon={loading ? 'loader' : ''}>
           Set domain
         </Button>

+ 5 - 3
client/reducers/__test__/settings.js

@@ -15,6 +15,7 @@ describe('settings reducer', () => {
   const initialState = {
     apikey: '',
     customDomain: '',
+    homepage: '',
     domainInput: true
   };
 
@@ -27,15 +28,16 @@ describe('settings reducer', () => {
   });
 
   it('should handle SET_DOMAIN', () => {
-    const domain = 'example.com';
+    const customDomain = 'example.com';
+    const homepage = '';
 
     const state = reducer(initialState, {
       type: SET_DOMAIN,
-      payload: domain
+      payload: { customDomain, homepage }
     });
 
     expect(state).not.to.be.undefined;
-    expect(state.customDomain).to.be.equal(domain);
+    expect(state.customDomain).to.be.equal(customDomain);
     expect(state.domainInput).to.be.false;
   });
 

+ 8 - 2
client/reducers/settings.js

@@ -9,17 +9,23 @@ import {
 const initialState = {
   apikey: '',
   customDomain: '',
+  homepage: '',
   domainInput: true,
 };
 
 const settings = (state = initialState, action) => {
   switch (action.type) {
     case SET_DOMAIN:
-      return { ...state, customDomain: action.payload, domainInput: false };
+      return {
+        ...state,
+        customDomain: action.payload.customDomain,
+        homepage: action.payload.homepage,
+        domainInput: false,
+      };
     case SET_APIKEY:
       return { ...state, apikey: action.payload };
     case DELETE_DOMAIN:
-      return { ...state, customDomain: '', domainInput: true };
+      return { ...state, customDomain: '', homepage: '', domainInput: true };
     case SHOW_DOMAIN_INPUT:
       return { ...state, domainInput: true };
     case UNAUTH_USER:

+ 5 - 1
server/controllers/authController.js

@@ -170,7 +170,11 @@ exports.generateApiKey = async ({ user }, res) => {
 };
 
 exports.userSettings = ({ user }, res) =>
-  res.status(200).json({ apikey: user.apikey || '', customDomain: user.domain || '' });
+  res.status(200).json({
+    apikey: user.apikey || '',
+    customDomain: user.domain || '',
+    homepage: user.homepage || '',
+  });
 
 exports.requestPasswordReset = async ({ body: { email } }, res) => {
   const user = await requestPasswordReset({ email });

+ 41 - 10
server/controllers/urlController.js

@@ -25,6 +25,7 @@ const {
   getBannedDomain,
   getBannedHost,
 } = require('../db/url');
+const { preservedUrls } = require('./validateBodyController');
 const transporter = require('../mail/mail');
 const redis = require('../redis');
 const { addProtocol, generateShortUrl, getStatsCacheTime } = require('../utils');
@@ -134,11 +135,18 @@ exports.goToUrl = async (req, res, next) => {
     url = JSON.parse(cachedUrl);
   } else {
     const urls = await findUrl({ id, domain });
-    if (!urls && !urls.length) return next();
-    url = urls.find(item => (domain ? item.domain === domain : !item.domain));
+    url =
+      urls && urls.length && urls.find(item => (domain ? item.domain === domain : !item.domain));
   }
 
-  if (!url) return next();
+  if (!url) {
+    if (host !== config.DEFAULT_DOMAIN) {
+      const { homepage } = await getCustomDomain({ customDomain: domain });
+      if (!homepage) return next();
+      return res.redirect(301, homepage);
+    }
+    return next();
+  }
 
   redis.set(id + (domain || ''), JSON.stringify(url), 'EX', 60 * 60 * 1);
 
@@ -211,23 +219,33 @@ exports.getUrls = async ({ query, user }, res) => {
   return res.json({ list, countAll });
 };
 
-exports.setCustomDomain = async ({ body: { customDomain }, user }, res) => {
+exports.setCustomDomain = async ({ body, user }, res) => {
+  const parsed = URL.parse(body.customDomain);
+  const customDomain = parsed.hostname || parsed.href;
+  if (!customDomain) return res.status(400).json({ error: 'Domain is not valid.' });
   if (customDomain.length > 40) {
     return res.status(400).json({ error: 'Maximum custom domain length is 40.' });
   }
   if (customDomain === config.DEFAULT_DOMAIN) {
     return res.status(400).json({ error: "You can't use default domain." });
   }
-  const isValidDomain = urlRegex({ exact: true, strict: false }).test(customDomain);
-  if (!isValidDomain) return res.status(400).json({ error: 'Domain is not valid.' });
-  const isOwned = await getCustomDomain({ customDomain });
-  if (isOwned && isOwned.email !== user.email) {
+  const isValidHomepage =
+    !body.homepage || urlRegex({ exact: true, strict: false }).test(body.homepage);
+  if (!isValidHomepage) return res.status(400).json({ error: 'Homepage is not valid.' });
+  const homepage =
+    body.homepage &&
+    (URL.parse(body.homepage).protocol ? body.homepage : `http://${body.homepage}`);
+  const { email } = await getCustomDomain({ customDomain });
+  if (email && email !== user.email) {
     return res
       .status(400)
       .json({ error: 'Domain is already taken. Contact us for multiple users.' });
   }
-  const userCustomDomain = await setCustomDomain({ user, customDomain });
-  if (userCustomDomain) return res.status(201).json({ customDomain: userCustomDomain.name });
+  const userCustomDomain = await setCustomDomain({ user, customDomain, homepage });
+  if (userCustomDomain)
+    return res
+      .status(201)
+      .json({ customDomain: userCustomDomain.name, homepage: userCustomDomain.homepage });
   return res.status(400).json({ error: "Couldn't set custom domain." });
 };
 
@@ -237,6 +255,19 @@ exports.deleteCustomDomain = async ({ user }, res) => {
   return res.status(400).json({ error: "Couldn't delete custom domain." });
 };
 
+exports.customDomainRedirection = async (req, res, next) => {
+  const { headers, path } = req;
+  if (
+    headers.host !== config.DEFAULT_DOMAIN &&
+    (path === '/' ||
+      preservedUrls.filter(u => u !== 'url-password').some(item => item === path.replace('/', '')))
+  ) {
+    const { homepage } = await getCustomDomain({ customDomain: headers.host });
+    return res.redirect(301, homepage || `http://${config.DEFAULT_DOMAIN + path}`);
+  }
+  return next();
+};
+
 exports.deleteUrl = async ({ body: { id, domain }, user }, res) => {
   if (!id) return res.status(400).json({ error: 'No id has been provided.' });
   const customDomain = domain !== config.DEFAULT_DOMAIN && domain;

+ 5 - 0
server/controllers/validateBodyController.js

@@ -40,6 +40,7 @@ const preservedUrls = [
   'reset-password',
   'resetpassword',
   'url-password',
+  'url-info',
   'settings',
   'stats',
   'verify',
@@ -47,6 +48,10 @@ const preservedUrls = [
   '404',
   'static',
   'images',
+  'banned',
+  'terms',
+  'privacy',
+  'report',
 ];
 
 exports.preservedUrls = preservedUrls;

+ 15 - 6
server/db/url.js

@@ -197,19 +197,27 @@ exports.getCustomDomain = ({ customDomain }) =>
     const session = driver.session();
     session
       .readTransaction(tx =>
-        tx.run('MATCH (d:DOMAIN { name: $customDomain })<-[:OWNS]-(u) RETURN u', {
-          customDomain,
-        })
+        tx.run(
+          'MATCH (d:DOMAIN { name: $customDomain })<-[:OWNS]-(u) RETURN u.email as email, d.homepage as homepage',
+          {
+            customDomain,
+          }
+        )
       )
       .then(({ records }) => {
         session.close();
-        const data = records.length && records[0].get('u').properties;
+        const data = records.length
+          ? {
+              email: records[0].get('email'),
+              homepage: records[0].get('homepage'),
+            }
+          : {};
         resolve(data);
       })
       .catch(err => session.close() || reject(err));
   });
 
-exports.setCustomDomain = ({ user, customDomain }) =>
+exports.setCustomDomain = ({ user, customDomain, homepage }) =>
   new Promise((resolve, reject) => {
     const session = driver.session();
     session
@@ -217,10 +225,11 @@ exports.setCustomDomain = ({ user, customDomain }) =>
         tx.run(
           'MATCH (u:USER { email: $email }) ' +
             'OPTIONAL MATCH (u)-[r:OWNS]->() DELETE r ' +
-            'MERGE (d:DOMAIN { name: $customDomain }) ' +
+            `MERGE (d:DOMAIN { name: $customDomain, homepage: $homepage }) ` +
             'MERGE (u)-[:OWNS]->(d) RETURN u, d',
           {
             customDomain,
+            homepage: homepage || '',
             email: user.email,
           }
         )

+ 2 - 1
server/db/user.js

@@ -21,7 +21,8 @@ exports.getUser = ({ email = '', apikey = '' }) =>
         const user = res.records.length && res.records[0].get('u').properties;
         const domainProps = res.records.length && res.records[0].get('l');
         const domain = domainProps ? domainProps.properties.name : '';
-        return resolve(user && { ...user, domain });
+        const homepage = domainProps ? domainProps.properties.homepage : '';
+        return resolve(user && { ...user, domain, homepage });
       })
       .catch(err => reject(err));
   });

+ 1 - 11
server/server.js

@@ -10,7 +10,6 @@ const cors = require('cors');
 const {
   validateBody,
   validationCriterias,
-  preservedUrls,
   validateUrl,
   cooldownCheck,
   malwareCheck,
@@ -60,16 +59,7 @@ app.prepare().then(() => {
     return next();
   });
 
-  server.use((req, res, next) => {
-    const { headers, path } = req;
-    if (
-      headers.host !== config.DEFAULT_DOMAIN &&
-      (path === '/' || preservedUrls.some(item => item === path.replace('/', '')))
-    ) {
-      return res.redirect(`http://${config.DEFAULT_DOMAIN + path}`);
-    }
-    return next();
-  });
+  server.use(url.customDomainRedirection);
 
   /* View routes */
   server.get('/', (req, res) => app.render(req, res, '/'));