diff options
author | Johannes Zellner <johannes@cloudron.io> | 2019-08-07 22:33:23 +0200 |
---|---|---|
committer | Johannes Zellner <johannes@cloudron.io> | 2019-08-07 22:33:23 +0200 |
commit | c2c00fca7dccb6e512a0f01bc87db129538765ef (patch) | |
tree | 3b872bed1587231a22d24fe8aaf3043565900f3b | |
parent | d5940df06a4f498176dad293f66607fb69eb2a28 (diff) | |
download | Surfer-c2c00fca7dccb6e512a0f01bc87db129538765ef.tar.gz Surfer-c2c00fca7dccb6e512a0f01bc87db129538765ef.tar.zst Surfer-c2c00fca7dccb6e512a0f01bc87db129538765ef.zip |
Add access token ui and rest api
-rw-r--r-- | frontend/css/style.css | 9 | ||||
-rw-r--r-- | frontend/index.html | 16 | ||||
-rw-r--r-- | frontend/js/app.js | 44 | ||||
-rwxr-xr-x | server.js | 3 | ||||
-rw-r--r-- | src/auth.js | 37 |
5 files changed, 105 insertions, 4 deletions
diff --git a/frontend/css/style.css b/frontend/css/style.css index 901de34..b43d6fe 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css | |||
@@ -89,3 +89,12 @@ a:hover, a:focus { | |||
89 | align-items: center; | 89 | align-items: center; |
90 | justify-content: center; | 90 | justify-content: center; |
91 | } | 91 | } |
92 | |||
93 | .access-token-input { | ||
94 | padding: 5px 0; | ||
95 | width: 450px; | ||
96 | } | ||
97 | |||
98 | .access-token-input > input, .access-token-input i { | ||
99 | cursor: copy !important; | ||
100 | } | ||
diff --git a/frontend/index.html b/frontend/index.html index 2d97e1c..20154da 100644 --- a/frontend/index.html +++ b/frontend/index.html | |||
@@ -36,6 +36,21 @@ | |||
36 | </span> | 36 | </span> |
37 | </el-dialog> | 37 | </el-dialog> |
38 | 38 | ||
39 | <el-dialog title="Access Tokens" :visible.sync="accessTokensDialogVisible" width="30%"> | ||
40 | Tokens can be used with the surfer <a href="https://www.npmjs.com/package/cloudron-surfer" target="_blank">cli tool</a> or using the Api directly. | ||
41 | They are shared between all users. | ||
42 | <br/> | ||
43 | <br/> | ||
44 | <div> | ||
45 | <div v-for="accessToken in accessTokens"> | ||
46 | <el-input suffix-icon="el-icon-copy-document" v-model="accessToken" class="access-token-input" @focus="onCopyAccessToken" size="small"></el-input> | ||
47 | <el-button icon="el-icon-delete" type="danger" size="small" @click="onDeleteAccessToken(accessToken)"></el-button> | ||
48 | </div> | ||
49 | </div> | ||
50 | <br/> | ||
51 | <el-button @click="onCreateAccessToken()" size="small" type="primary">Create Access Token</el-button> | ||
52 | </el-dialog> | ||
53 | |||
39 | <el-header> | 54 | <el-header> |
40 | <el-row type="flex" justify="space-between"> | 55 | <el-row type="flex" justify="space-between"> |
41 | <div style="padding: 7px;"> | 56 | <div style="padding: 7px;"> |
@@ -66,6 +81,7 @@ | |||
66 | </el-dropdown-item> | 81 | </el-dropdown-item> |
67 | <el-dropdown-item disabled divided>WebDAV Endpoint</el-dropdown-item> | 82 | <el-dropdown-item disabled divided>WebDAV Endpoint</el-dropdown-item> |
68 | <el-dropdown-item><a href="/_webdav/" target="_blank">{{ origin }}/_webdav/</a></el-dropdown-item> | 83 | <el-dropdown-item><a href="/_webdav/" target="_blank">{{ origin }}/_webdav/</a></el-dropdown-item> |
84 | <el-dropdown-item command="apiAccess" divided><i class="el-icon-connection"></i> Access Tokens</el-dropdown-item> | ||
69 | <el-dropdown-item command="about" divided><i class="el-icon-info"></i> About</el-dropdown-item> | 85 | <el-dropdown-item command="about" divided><i class="el-icon-info"></i> About</el-dropdown-item> |
70 | <el-dropdown-item command="logout" id="logoutButton"><i class="el-icon-circle-close"></i> Logout</el-dropdown-item> | 86 | <el-dropdown-item command="logout" id="logoutButton"><i class="el-icon-circle-close"></i> Logout</el-dropdown-item> |
71 | </el-dropdown-menu> | 87 | </el-dropdown-menu> |
diff --git a/frontend/js/app.js b/frontend/js/app.js index d99a840..05cbe9c 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js | |||
@@ -43,6 +43,8 @@ function initWithToken(accessToken) { | |||
43 | app.folderListingEnabled = !!result.body.folderListingEnabled; | 43 | app.folderListingEnabled = !!result.body.folderListingEnabled; |
44 | 44 | ||
45 | loadDirectory(decode(window.location.hash.slice(1))); | 45 | loadDirectory(decode(window.location.hash.slice(1))); |
46 | |||
47 | app.refreshAccessTokens(); | ||
46 | }); | 48 | }); |
47 | }); | 49 | }); |
48 | } | 50 | } |
@@ -278,7 +280,9 @@ var app = new Vue({ | |||
278 | password: '', | 280 | password: '', |
279 | busy: false | 281 | busy: false |
280 | }, | 282 | }, |
281 | entries: [] | 283 | entries: [], |
284 | accessTokens: [], | ||
285 | accessTokensDialogVisible: false | ||
282 | }, | 286 | }, |
283 | methods: { | 287 | methods: { |
284 | onLogin: function () { | 288 | onLogin: function () { |
@@ -312,6 +316,8 @@ var app = new Vue({ | |||
312 | }).then(function () {}).catch(function () {}); | 316 | }).then(function () {}).catch(function () {}); |
313 | } else if (command === 'logout') { | 317 | } else if (command === 'logout') { |
314 | logout(); | 318 | logout(); |
319 | } else if (command === 'apiAccess') { | ||
320 | this.accessTokensDialogVisible = true; | ||
315 | } | 321 | } |
316 | }, | 322 | }, |
317 | onDownload: function (entry) { | 323 | onDownload: function (entry) { |
@@ -415,6 +421,42 @@ var app = new Vue({ | |||
415 | }); | 421 | }); |
416 | }).catch(function () {}); | 422 | }).catch(function () {}); |
417 | }, | 423 | }, |
424 | refreshAccessTokens: function () { | ||
425 | var that = this; | ||
426 | |||
427 | superagent.get('/api/tokens').query({ access_token: localStorage.accessToken }).end(function (error, result) { | ||
428 | if (error && !result) return that.$message.error(error.message); | ||
429 | |||
430 | that.accessTokens = result.body.accessTokens; | ||
431 | }); | ||
432 | }, | ||
433 | onCopyAccessToken: function (event) { | ||
434 | event.target.select(); | ||
435 | document.execCommand('copy'); | ||
436 | |||
437 | this.$message({ type: 'success', message: 'Access token copied to clipboard' }); | ||
438 | }, | ||
439 | onCreateAccessToken: function () { | ||
440 | var that = this; | ||
441 | |||
442 | superagent.post('/api/tokens').query({ access_token: localStorage.accessToken }).end(function (error, result) { | ||
443 | if (error && !result) return that.$message.error(error.message); | ||
444 | |||
445 | that.refreshAccessTokens(); | ||
446 | }); | ||
447 | }, | ||
448 | onDeleteAccessToken: function (token) { | ||
449 | var that = this; | ||
450 | |||
451 | this.$confirm('All actions from apps using this token will fail!', 'Really delete this access token?', { confirmButtonText: 'Yes Delete', cancelButtonText: 'No' }).then(function () { | ||
452 | superagent.delete('/api/tokens/' + token).query({ access_token: localStorage.accessToken }).end(function (error, result) { | ||
453 | if (error && !result) return that.$message.error(error.message); | ||
454 | |||
455 | that.refreshAccessTokens(); | ||
456 | }); | ||
457 | }).catch(function () {}); | ||
458 | |||
459 | }, | ||
418 | prettyDate: function (row, column, cellValue, index) { | 460 | prettyDate: function (row, column, cellValue, index) { |
419 | var date = new Date(cellValue), | 461 | var date = new Date(cellValue), |
420 | diff = (((new Date()).getTime() - date.getTime()) / 1000), | 462 | diff = (((new Date()).getTime() - date.getTime()) / 1000), |
@@ -77,6 +77,9 @@ router.post ('/api/login', auth.login); | |||
77 | router.post ('/api/logout', auth.verify, auth.logout); | 77 | router.post ('/api/logout', auth.verify, auth.logout); |
78 | router.get ('/api/settings', auth.verify, getSettings); | 78 | router.get ('/api/settings', auth.verify, getSettings); |
79 | router.put ('/api/settings', auth.verify, setSettings); | 79 | router.put ('/api/settings', auth.verify, setSettings); |
80 | router.get ('/api/tokens', auth.verify, auth.getTokens); | ||
81 | router.post ('/api/tokens', auth.verify, auth.createToken); | ||
82 | router.delete('/api/tokens/:token', auth.verify, auth.delToken); | ||
80 | router.get ('/api/profile', auth.verify, auth.getProfile); | 83 | router.get ('/api/profile', auth.verify, auth.getProfile); |
81 | router.get ('/api/files/*', auth.verify, files.get); | 84 | router.get ('/api/files/*', auth.verify, files.get); |
82 | router.post ('/api/files/*', auth.verify, multipart, files.post); | 85 | router.post ('/api/files/*', auth.verify, multipart, files.post); |
diff --git a/src/auth.js b/src/auth.js index 2532688..a885d49 100644 --- a/src/auth.js +++ b/src/auth.js | |||
@@ -15,6 +15,8 @@ const LDAP_USERS_BASE_DN = process.env.CLOUDRON_LDAP_USERS_BASE_DN; | |||
15 | const LOCAL_AUTH_FILE = path.resolve(process.env.LOCAL_AUTH_FILE || './.users.json'); | 15 | const LOCAL_AUTH_FILE = path.resolve(process.env.LOCAL_AUTH_FILE || './.users.json'); |
16 | const TOKENSTORE_FILE = path.resolve(process.env.TOKENSTORE_FILE || './.tokens.json'); | 16 | const TOKENSTORE_FILE = path.resolve(process.env.TOKENSTORE_FILE || './.tokens.json'); |
17 | const AUTH_METHOD = (LDAP_URL && LDAP_USERS_BASE_DN) ? 'ldap' : 'local'; | 17 | const AUTH_METHOD = (LDAP_URL && LDAP_USERS_BASE_DN) ? 'ldap' : 'local'; |
18 | const LOGIN_TOKEN_PREFIX = 'login-'; | ||
19 | const API_TOKEN_PREFIX = 'api-'; | ||
18 | 20 | ||
19 | if (AUTH_METHOD === 'ldap') { | 21 | if (AUTH_METHOD === 'ldap') { |
20 | console.log('Use ldap auth'); | 22 | console.log('Use ldap auth'); |
@@ -34,8 +36,11 @@ var tokenStore = { | |||
34 | get: function (token, callback) { | 36 | get: function (token, callback) { |
35 | callback(tokenStore.data[token] ? null : 'not found', tokenStore.data[token]); | 37 | callback(tokenStore.data[token] ? null : 'not found', tokenStore.data[token]); |
36 | }, | 38 | }, |
37 | set: function (token, data, callback) { | 39 | getApiTokens: function (callback) { |
38 | tokenStore.data[token] = data; | 40 | callback(null, Object.keys(tokenStore.data).filter(function (t) { return t.indexOf(API_TOKEN_PREFIX) === 0; })) |
41 | }, | ||
42 | set: function (token, user, callback) { | ||
43 | tokenStore.data[token] = user; | ||
39 | tokenStore.save(); | 44 | tokenStore.save(); |
40 | callback(null); | 45 | callback(null); |
41 | }, | 46 | }, |
@@ -102,7 +107,7 @@ exports.login = function (req, res, next) { | |||
102 | verifyUser(req.body.username, req.body.password, function (error, user) { | 107 | verifyUser(req.body.username, req.body.password, function (error, user) { |
103 | if (error) return next(new HttpError(401, 'Invalid credentials')); | 108 | if (error) return next(new HttpError(401, 'Invalid credentials')); |
104 | 109 | ||
105 | var accessToken = uuid(); | 110 | var accessToken = LOGIN_TOKEN_PREFIX + uuid(); |
106 | 111 | ||
107 | tokenStore.set(accessToken, user, function (error) { | 112 | tokenStore.set(accessToken, user, function (error) { |
108 | if (error) return next(new HttpError(500, error)); | 113 | if (error) return next(new HttpError(500, error)); |
@@ -139,6 +144,32 @@ exports.getProfile = function (req, res, next) { | |||
139 | next(new HttpSuccess(200, { username: req.user.username })); | 144 | next(new HttpSuccess(200, { username: req.user.username })); |
140 | }; | 145 | }; |
141 | 146 | ||
147 | exports.getTokens = function (req, res, next) { | ||
148 | tokenStore.getApiTokens(function (error, result) { | ||
149 | if (error) return next(new HttpError(500, error)); | ||
150 | |||
151 | next(new HttpSuccess(200, { accessTokens: result })); | ||
152 | }); | ||
153 | }; | ||
154 | |||
155 | exports.createToken = function (req, res, next) { | ||
156 | var accessToken = API_TOKEN_PREFIX + uuid(); | ||
157 | |||
158 | tokenStore.set(accessToken, req.user, function (error) { | ||
159 | if (error) return next(new HttpError(500, error)); | ||
160 | |||
161 | next(new HttpSuccess(201, { accessToken: accessToken })); | ||
162 | }); | ||
163 | }; | ||
164 | |||
165 | exports.delToken = function (req, res, next) { | ||
166 | tokenStore.del(req.params.token, function (error) { | ||
167 | if (error) console.error(error); | ||
168 | |||
169 | next(new HttpSuccess(200, {})); | ||
170 | }); | ||
171 | }; | ||
172 | |||
142 | // webdav usermanager | 173 | // webdav usermanager |
143 | exports.WebdavUserManager = WebdavUserManager; | 174 | exports.WebdavUserManager = WebdavUserManager; |
144 | 175 | ||