Browse Source

Update node server and frontend to use API v4

Thomas Dy 7 years ago
parent
commit
aa492c6cf1
7 changed files with 125 additions and 98 deletions
  1. 1 2
      lib/App.jsx
  2. 21 20
      lib/cloudflare.js
  3. 25 34
      lib/stores.js
  4. 3 3
      lib/ui/DomainList.jsx
  5. 21 16
      lib/ui/RecordList.jsx
  6. 14 8
      lib/ui/Settings.jsx
  7. 40 15
      server.js

+ 1 - 2
lib/App.jsx

@@ -31,8 +31,7 @@ var App = React.createClass({
       var store = DomainStore.find(domain);
       if(store) {
         if(settings) {
-          DomainStore.loadSettings(domain);
-          content.push(<Settings key="settings" domain={domain} settings={store.settings} />);
+          content.push(<Settings key="settings" domain={domain} settings={store} />);
         }
         else {
           DomainStore.loadRecords(domain);

+ 21 - 20
lib/cloudflare.js

@@ -1,37 +1,38 @@
 var reqwest = require('reqwest');
 var assign = require('react/lib/Object.assign');
 
-function makeCall(path) {
+function makeCall(method, path) {
   return function() {
-    var domain = null;
+    var localPath = path;
+    var pathParams = null;
     var options = {};
     if(arguments.length > 0) {
-      domain = arguments[0];
+      pathParams = arguments[0];
     }
     if(arguments.length > 1) {
       options = arguments[1];
     }
+    if(typeof pathParams != "object") {
+      pathParams = {zoneId: pathParams};
+    }
+    for(var key in pathParams) {
+      localPath = localPath.replace(':'+key, pathParams[key]);
+    }
     return reqwest({
-      url: '/api',
-      data: assign({a: path, z: domain}, options),
-      method: 'POST'
+      url: '/api'+localPath,
+      data: method == 'GET' ? null : JSON.stringify(options),
+      method: method,
+      contentType: 'application/json'
     });
   };
 }
 
 module.exports = {
-  domains: makeCall('zone_load_multi'),
-  settings: makeCall('zone_settings'),
-  records: makeCall('rec_load_all'),
-  recordAdd: makeCall('rec_new'),
-  recordEdit: makeCall('rec_edit'),
-  recordDelete: function(domain, id) {
-    return makeCall('rec_delete')(domain, {id: id});
-  },
-  setDevelopmentMode: function(domain, toggle) {
-    return makeCall('devmode')(domain, {v: toggle ? 1 : 0});
-  },
-  purgeCache: function(domain) {
-    return makeCall('fpurge_ts')(domain, {v: 1});
-  }
+  domains: makeCall('GET', '/zones'),
+  records: makeCall('GET', '/zones/:zoneId/dns_records'),
+  recordAdd: makeCall('POST', '/zones/:zoneId/dns_records'),
+  recordEdit: makeCall('PUT', '/zones/:zoneId/dns_records/:recId'),
+  recordDelete: makeCall('DELETE', '/zones/:zoneId/dns_records/:recId'),
+  setDevelopmentMode: makeCall('PATCH', '/zones/:zoneId/settings/development_mode'),
+  purgeCache: makeCall('DELETE', '/zones/:zoneId/purge_cache')
 };

+ 25 - 34
lib/stores.js

@@ -5,7 +5,7 @@ var DomainCortex = new Cortex([]);
 
 function findDomain(name) {
   return DomainCortex.find(function(d) {
-    return d.zone_name.val() === name;
+    return d.name.val() === name;
   });
 }
 
@@ -14,47 +14,38 @@ function loadRecords(name) {
   if(domain.records.count() > 0) {
     return;
   }
-  cloudflare.records(name).then(function(data) {
-    domain.records.set(data.response.recs.objs);
-  });
-}
-
-function loadSettings(name) {
-  var domain = findDomain(name);
-  if(domain.settings.val()) {
-    return;
-  }
-  cloudflare.settings(name).then(function(data) {
-    domain.settings.set(data.response.result.objs[0]);
+  cloudflare.records(domain.id.val()).then(function(data) {
+    domain.records.set(data.result);
   });
 }
 
 function recordAdd(name, record) {
-  return cloudflare.recordAdd(name, record).then(function(data) {
-    if(data.result === 'success') {
-      findDomain(name).records.push(data.response.rec.obj);
+  var domain = findDomain(name);
+  return cloudflare.recordAdd(domain.id.val(), record).then(function(data) {
+    if(data.success) {
+      domain.records.push(data.result);
     }
   });
 }
 
 function recordEdit(name, record) {
-  return cloudflare.recordEdit(name, record).then(function(data) {
-    if(data.result === 'success') {
-      var domain = findDomain(name);
+  var domain = findDomain(name);
+  return cloudflare.recordEdit({zoneId: domain.id.val(), recId: record.id}, record).then(function(data) {
+    if(data.success) {
       var oldRecord = domain.records.find(function(r) {
-        return r.rec_id.val() === record.id;
+        return r.id.val() === record.id;
       });
-      oldRecord.set(data.response.rec.obj);
+      oldRecord.set(data.result);
     }
   });
 }
 
 function recordDelete(name, id) {
-  return cloudflare.recordDelete(name, id).then(function(data) {
-    if(data.result === 'success') {
-      var domain = findDomain(name);
+  var domain = findDomain(name);
+  return cloudflare.recordDelete({zoneId: domain.id.val(), recId: id}).then(function(data) {
+    if(data.success) {
       var oldRecord = domain.records.find(function(r) {
-        return r.rec_id.val() === id;
+        return r.id.val() === id;
       });
       oldRecord.remove();
     }
@@ -62,23 +53,24 @@ function recordDelete(name, id) {
 }
 
 function setDevelopmentMode(name, value) {
-  return cloudflare.setDevelopmentMode(name, value).then(function(data) {
-    if(data.result === 'success') {
-      findDomain(name).settings.dev_mode.set(data.response.expires_on || 0);
+  var domain = findDomain(name);
+  return cloudflare.setDevelopmentMode(domain.id.val(), {value: value ? 'on' : 'off'}).then(function(data) {
+    if(data.success) {
+      domain.development_mode.set(data.response.expires_on || 0);
     }
   });
 }
 
 function purgeCache(name) {
-  return cloudflare.purgeCache(name);
+  return cloudflare.purgeCache(findDomain(name).id.val(), {purge_everything: true});
 }
 
 cloudflare.domains().then(function(data) {
-  DomainCortex.set(data.response.zones.objs);
-  DomainCortex.forEach(function(element) {
-    element.add('records', []);
-    element.add('settings', false);
+  data.result.forEach(function(domain) {
+    domain.development_mode = Date.now() / 1000 + domain.development_mode;
+    domain.records = [];
   });
+  DomainCortex.set(data.result);
 });
 
 module.exports = {
@@ -90,7 +82,6 @@ module.exports = {
     setDevelopmentMode: setDevelopmentMode,
     purgeCache: purgeCache,
     loadRecords: loadRecords,
-    loadSettings: loadSettings,
     cortex: DomainCortex
   }
 };

+ 3 - 3
lib/ui/DomainList.jsx

@@ -5,7 +5,7 @@ var Domain = React.createClass({
     var className = this.props.active ? 'active' : '';
     return (
       <li role="presentation" className={className}>
-        <a href={'/'+this.props.data.zone_name.val()}>{this.props.data.zone_name.val()}</a>
+        <a href={'/'+this.props.data.name.val()}>{this.props.data.name.val()}</a>
       </li>
     );
   }
@@ -15,8 +15,8 @@ var DomainList = React.createClass({
   render: function() {
     var currDomain = this.props.currentDomain;
     var domains = this.props.domains.map(function(domain) {
-      var active = currDomain === domain.zone_name.val();
-      return <Domain key={domain.zone_id.val()} data={domain} active={active} />
+      var active = currDomain === domain.name.val();
+      return <Domain key={domain.id.val()} data={domain} active={active} />
     });
 
     return (

+ 21 - 16
lib/ui/RecordList.jsx

@@ -4,9 +4,8 @@ var React = require('react');
 var CloudActive = React.createClass({
   render: function() {
     var record = this.props.record;
-    var type = record.type.val();
-    if(type === 'A' || type === 'AAAA' || type === 'CNAME') {
-      var active = record.service_mode.val() === '1';
+    if(record.proxiable.val()) {
+      var active = record.proxied.val();
       if(active) {
         return <button className='btn btn-warning' onClick={this.props.onClick}>On</button>
       }
@@ -86,20 +85,20 @@ var Record = React.createClass({
   commitDelete: function() {
     this.setState({saving: true});
     var record = this.props.record;
-    DomainStore.recordDelete(record.zone_name.val(), record.rec_id.val());
+    DomainStore.recordDelete(record.zone_name.val(), record.id.val());
   },
   commitEdit: function() {
     this.setState({saving: true});
     var record = this.props.record;
     var newRecord = {
-      id: record.rec_id.val(),
+      id: record.id.val(),
       type: record.type.val(),
       name: this.refs.name.getDOMNode().value.trim(),
       content: this.refs.value.getDOMNode().value.trim(),
       ttl: 1
     };
-    if(record.service_mode.val()) {
-      newRecord.service_mode = record.service_mode.val();
+    if(record.proxied.val()) {
+      newRecord.proxied = record.proxied.val();
     }
     DomainStore.recordEdit(record.zone_name.val(), newRecord);
   },
@@ -107,11 +106,11 @@ var Record = React.createClass({
     this.setState({saving: true});
     var record = this.props.record;
     var newRecord = {
-      id: record.rec_id.val(),
+      id: record.id.val(),
       type: record.type.val(),
       name: record.name.val(),
       content: record.content.val(),
-      service_mode: record.service_mode.val() === "1" ? "0" : "1",
+      proxied: !record.proxied.val(),
       ttl: 1
     };
     DomainStore.recordEdit(record.zone_name.val(), newRecord);
@@ -120,12 +119,18 @@ var Record = React.createClass({
     var record = this.props.record;
     var className = this.state.saving ? 'saving' : '';
     var editDisabled = ['MX', 'SRV'].indexOf(record.type.val()) >= 0;
+    var displayName = record.name.val();
+    var zoneName = '.'+record.zone_name.val();
+    var limit = displayName.length - zoneName.length;
+    if(limit > 0 && displayName.substring(limit) === zoneName) {
+      displayName = displayName.substring(0, limit);
+    }
     if(this.state.state === 'edit') {
       return (
         <tr className={className}>
           <td className="record-type"><span className={record.type.val()}>{record.type.val()}</span></td>
-          <td><input type="text" ref="name" defaultValue={record.display_name.val()} /></td>
-          <td><input type="text" ref="value" defaultValue={record.display_content.val()} /></td>
+          <td><input type="text" ref="name" defaultValue={displayName} /></td>
+          <td><input type="text" ref="value" defaultValue={record.content.val()} /></td>
           <td>
             <a onClick={this.cancelEdit}>Cancel</a>
           </td>
@@ -139,8 +144,8 @@ var Record = React.createClass({
       return (
         <tr className={className}>
           <td className="record-type"><span className={record.type.val()}>{record.type.val()}</span></td>
-          <td><strong>{record.display_name.val()}</strong></td>
-          <td>{record.display_content.val()}</td>
+          <td><strong>{displayName}</strong></td>
+          <td>{record.content.val()}</td>
           <td>
             <a onClick={this.cancelEdit}>Cancel</a>
           </td>
@@ -154,8 +159,8 @@ var Record = React.createClass({
       return (
         <tr className={className}>
           <td className="record-type"><span className={record.type.val()}>{record.type.val()}</span></td>
-          <td><strong>{record.display_name.val()}</strong></td>
-          <td className="value">{record.display_content.val()}</td>
+          <td><strong>{displayName}</strong></td>
+          <td className="value">{record.content.val()}</td>
           <td><CloudActive record={record} onClick={this.toggleProxy} /></td>
           <td className="actions">
             <button className="btn btn-primary" disabled={editDisabled} onClick={this.setEditing}>Edit</button>
@@ -170,7 +175,7 @@ var Record = React.createClass({
 var RecordList = React.createClass({
   render: function() {
     var records = this.props.records.map(function(record) {
-      return <Record key={record.rec_id.val()} record={record} />
+      return <Record key={record.id.val()} record={record} />
     }.bind(this));
 
     var body;

+ 14 - 8
lib/ui/Settings.jsx

@@ -11,13 +11,13 @@ var DevModeToggle = React.createClass({
   },
   toggleDevMode: function() {
     this.setState({toggling: true});
-    DomainStore.setDevelopmentMode(this.props.domain, this.props.devMode == 0);
+    DomainStore.setDevelopmentMode(this.props.domain, this.props.devMode * 1000 <= Date.now());
   },
   render: function() {
     if(this.state.toggling) {
       return <button className="btn btn-warning" disabled>{this.props.devMode > 0 ? 'Disabling...' : 'Enabling...'}</button>
     }
-    else if(this.props.devMode > 0) {
+    else if(this.props.devMode * 1000 > Date.now()) {
       var date = new Date(this.props.devMode*1000);
       return (
         <div>
@@ -33,20 +33,26 @@ var DevModeToggle = React.createClass({
 });
 var PurgeButton = React.createClass({
   getInitialState: function() {
-    return {purging: false};
+    return {purging: false, failed: false};
   },
   purgeCache: function() {
+    var self = this;
     this.setState({purging: true});
     var reset = function() {
-      this.setState({purging: false});
+      this.setState({purging: false, failed: false});
     }.bind(this);
     DomainStore.purgeCache(this.props.domain).then(function(data) {
-      var timeout = data.attributes.cooldown;
-      setTimeout(reset, timeout*1000);
+      setTimeout(reset, 5*1000);
+    }, function(error) {
+      self.setState({failed: true});
+      setTimeout(reset, 10*1000);
     });
   },
   render: function() {
-    if(this.state.purging) {
+    if(this.state.failed) {
+      return <button className="btn btn-danger" disabled>Purge failed</button>
+    }
+    else if(this.state.purging) {
       return <button className="btn btn-warning" disabled>Purging...</button>
     }
     else {
@@ -65,7 +71,7 @@ var Settings = React.createClass({
         <table className="table">
           <tr>
             <td>Development Mode</td>
-            <td><DevModeToggle domain={this.props.domain} devMode={this.props.settings.dev_mode.val()} /></td>
+            <td><DevModeToggle domain={this.props.domain} devMode={this.props.settings.development_mode.val()} /></td>
           </tr>
           <tr>
             <td>Purge Cache</td>

+ 40 - 15
server.js

@@ -5,9 +5,10 @@ var serveStatic = require('serve-static');
 var bodyParser = require('body-parser');
 var config = require('./config.json');
 
-var apiEndpoint = 'https://www.cloudflare.com/api_json.html';
+var apiEndpoint = 'https://api.cloudflare.com/client/v4';
 var isProd = config.isProd === undefined || config.isProd;
 var port = config.port || 8000;
+var identifierWhitelist = null;
 
 var app = connect();
 var serve = serveStatic('.', {'index': []});
@@ -23,31 +24,55 @@ var serveIndex = function(req, res, next) {
     }
 };
 
-app.use(bodyParser.urlencoded({extended: false}));
+app.use(bodyParser.json());
 app.use(function(req, res, next) {
-    if(req.url === '/api') {
-        req.body.email = config.email;
-        req.body.tkn = config.token;
+    if(req.url.startsWith('/api')) {
+        var headers = {
+            'X-Auth-Email': config.email,
+            'X-Auth-Key': config.token
+        }
+
+        var path = req.url.substring(4);
 
         // filter out only zones in the whitelist
-        if(req.body.a === 'zone_load_multi') {
-            request.post({uri: apiEndpoint, form: req.body, json: true}, function(err, inc, body) {
-                var filtered = body.response.zones.objs.filter(function(zone) {
-                    return config.whitelist.indexOf(zone.zone_name) >= 0;
+        if(path === '/zones') {
+            request.get({uri: apiEndpoint+path, headers: headers, json: true}, function(err, inc, body) {
+                var filtered = body.result.filter(function(zone) {
+                    return config.whitelist.indexOf(zone.name) >= 0;
                 });
-                body.response.zones.objs = filtered;
-                body.response.zones.count = filtered.length;
+                // TODO prefetch entire zone list
+                if(identifierWhitelist === null) {
+                    identifierWhitelist = filtered.map(function(zone) {
+                        return zone.id;
+                    });
+                }
+                body.result = filtered;
+                body.result_info.count = filtered.length;
                 res.setHeader('Content-Type', 'application/json');
                 res.end(JSON.stringify(body));
             });
         }
 
-        // allow any requests for zones in whitelist
-        else if(config.whitelist.indexOf(req.body.z) >= 0) {
-            request.post(apiEndpoint).form(req.body).pipe(res);
+        else if(path.startsWith('/zones')) {
+            // allow any requests for zones in whitelist
+            var identifier = path.replace(/^\/zones\/([0-9a-f]+).*$/, "$1");
+            if(identifierWhitelist.indexOf(identifier) >= 0) {
+                request({
+                    method: req.method,
+                    uri: apiEndpoint+path,
+                    headers: headers,
+                    qs: req.method == 'GET' ? {per_page: 999} : {},
+                    body: req.body,
+                    json: true
+                }).pipe(res);
+            }
+            else {
+                // deny otherwise
+                next();
+            }
         }
 
-        // deny otherwise
+        // deny other request paths for now
         else {
             next();
         }