mirror of https://github.com/keeweb/keeweb.git
added teams storage; added tenant id configuration for onedrive and teams
This commit is contained in:
parent
dcbe8d70da
commit
98268f21c0
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>KeeWeb</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.opener.postMessage(
|
||||||
|
{ storage: 'teams', search: location.search },
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
window.close();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -87,7 +87,13 @@ const DefaultAppSettings = {
|
||||||
|
|
||||||
onedrive: true, // enable OneDrive integration
|
onedrive: true, // enable OneDrive integration
|
||||||
onedriveClientId: null, // custom OneDrive client id
|
onedriveClientId: null, // custom OneDrive client id
|
||||||
onedriveClientSecret: null // custom OneDrive client secret
|
onedriveClientSecret: null, // custom OneDrive client secret
|
||||||
|
onedriveTenantId: null, // custom OneDrive tenant id
|
||||||
|
|
||||||
|
teams: true, // enable Teams integration
|
||||||
|
teamsClientId: null, // custom Teams client id
|
||||||
|
teamsClientSecret: null, // custom Teams client secret
|
||||||
|
teamsTenantId: null // custom Teams tenant id
|
||||||
};
|
};
|
||||||
|
|
||||||
export { DefaultAppSettings };
|
export { DefaultAppSettings };
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"dropbox": "Dropbox",
|
"dropbox": "Dropbox",
|
||||||
"gdrive": "Google Drive",
|
"gdrive": "Google Drive",
|
||||||
"onedrive": "OneDrive",
|
"onedrive": "OneDrive",
|
||||||
|
"teams": "Teams",
|
||||||
"menuAllItems": "All Items",
|
"menuAllItems": "All Items",
|
||||||
"menuColors": "Colors",
|
"menuColors": "Colors",
|
||||||
"menuTrash": "Trash",
|
"menuTrash": "Trash",
|
||||||
|
|
|
@ -221,22 +221,26 @@ class StorageOneDrive extends StorageBase {
|
||||||
_getOAuthConfig() {
|
_getOAuthConfig() {
|
||||||
let clientId = this.appSettings.onedriveClientId;
|
let clientId = this.appSettings.onedriveClientId;
|
||||||
let clientSecret = this.appSettings.onedriveClientSecret;
|
let clientSecret = this.appSettings.onedriveClientSecret;
|
||||||
|
let tenant = this.appSettings.onedriveTenantId;
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
if (Features.isDesktop) {
|
if (Features.isDesktop) {
|
||||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Desktop);
|
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Desktop);
|
||||||
} else if (Features.isLocal) {
|
} else if (Features.isLocal) {
|
||||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Local);
|
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Local);
|
||||||
} else {
|
} else {
|
||||||
({ id: clientId, secret: clientSecret } = OneDriveApps.Production);
|
({ id: clientId, secret: clientSecret, tenantId: tenant } = OneDriveApps.Production);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tenant = tenant || 'common';
|
||||||
|
|
||||||
let scope = 'files.readwrite';
|
let scope = 'files.readwrite';
|
||||||
if (!this.appSettings.shortLivedStorageToken) {
|
if (!this.appSettings.shortLivedStorageToken) {
|
||||||
scope += ' offline_access';
|
scope += ' offline_access';
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
||||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||||
scope,
|
scope,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
|
|
|
@ -0,0 +1,372 @@
|
||||||
|
import { StorageBase } from 'storage/storage-base';
|
||||||
|
import { TeamsApps } from 'const/cloud-storage-apps';
|
||||||
|
import { Features } from 'util/features';
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||||
|
|
||||||
|
// https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true
|
||||||
|
// https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified')
|
||||||
|
// https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified')&$orderby=displayName
|
||||||
|
// https://graph.microsoft.com/v1.0/groups?$orderby=displayName
|
||||||
|
// /me/joinedTeams
|
||||||
|
// https://graph.microsoft.com/v1.0/groups/{group id}/drive/root/children
|
||||||
|
|
||||||
|
class StorageTeams extends StorageBase {
|
||||||
|
name = 'teams';
|
||||||
|
enabled = true;
|
||||||
|
uipos = 50;
|
||||||
|
icon = 'user-friends';
|
||||||
|
|
||||||
|
_graphUrl = 'https://graph.microsoft.com/v1.0';
|
||||||
|
_groupsUrl = `${this._graphUrl}/me/joinedTeams`;
|
||||||
|
_baseUrl = `${this._graphUrl}/groups`;
|
||||||
|
|
||||||
|
getPathForName(fileName) {
|
||||||
|
return '/drive/root:/' + fileName + '.kdbx';
|
||||||
|
}
|
||||||
|
|
||||||
|
genUrlAddress(groupId, path) {
|
||||||
|
if (groupId) {
|
||||||
|
return this._baseUrl + '/' + groupId + (path ? '/' + path.replace(/^\/+/, '') : '');
|
||||||
|
} else {
|
||||||
|
return this._groupsUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genUrl(path) {
|
||||||
|
// console.warn('genUrl', path);
|
||||||
|
if (!path) {
|
||||||
|
const groupId = null;
|
||||||
|
const dir = null;
|
||||||
|
const url = this.genUrlAddress(groupId, dir);
|
||||||
|
return [groupId, dir, url];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = path.replace(/^\/+/, '').split('/');
|
||||||
|
if (parts.length === 0) {
|
||||||
|
const groupId = null;
|
||||||
|
const dir = null;
|
||||||
|
const url = this.genUrlAddress(groupId, dir);
|
||||||
|
return [groupId, dir, url];
|
||||||
|
} else if (parts.length === 1) {
|
||||||
|
const groupId = parts[0];
|
||||||
|
const dir = null;
|
||||||
|
const url = this.genUrlAddress(groupId, dir);
|
||||||
|
return [groupId, dir, url];
|
||||||
|
} else {
|
||||||
|
path = path.replace(/\/drive\/root\:/, '');
|
||||||
|
const groupId = parts[0];
|
||||||
|
const dir = ('/' + parts.slice(1).join('/')).replace(/^\/+/, '');
|
||||||
|
const url = this.genUrlAddress(groupId, dir);
|
||||||
|
return [groupId, dir, url];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load(path, opts, callback) {
|
||||||
|
this._oauthAuthorize((err) => {
|
||||||
|
if (err) {
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
this.logger.debug('Load', path);
|
||||||
|
const ts = this.logger.ts();
|
||||||
|
|
||||||
|
const urlParts = this.genUrl(path);
|
||||||
|
const groupId = urlParts[0];
|
||||||
|
path = urlParts[1];
|
||||||
|
const url = urlParts[2];
|
||||||
|
if (!groupId) {
|
||||||
|
const err = 'no group id defined';
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._xhr({
|
||||||
|
url,
|
||||||
|
responseType: 'json',
|
||||||
|
success: (response) => {
|
||||||
|
const downloadUrl = response['@microsoft.graph.downloadUrl'];
|
||||||
|
let rev = response.eTag;
|
||||||
|
if (!downloadUrl || !response.eTag) {
|
||||||
|
this.logger.debug(
|
||||||
|
'Load error',
|
||||||
|
path,
|
||||||
|
'no download url',
|
||||||
|
response,
|
||||||
|
this.logger.ts(ts)
|
||||||
|
);
|
||||||
|
return callback && callback('no download url');
|
||||||
|
}
|
||||||
|
this._xhr({
|
||||||
|
url: downloadUrl,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
skipAuth: true,
|
||||||
|
success: (response, xhr) => {
|
||||||
|
rev = xhr.getResponseHeader('ETag') || rev;
|
||||||
|
this.logger.debug('Loaded', path, rev, this.logger.ts(ts));
|
||||||
|
return callback && callback(null, response, { rev });
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stat(path, opts, callback) {
|
||||||
|
this._oauthAuthorize((err) => {
|
||||||
|
if (err) {
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
this.logger.debug('Stat', path);
|
||||||
|
const ts = this.logger.ts();
|
||||||
|
|
||||||
|
const urlParts = this.genUrl(path);
|
||||||
|
const groupId = urlParts[0];
|
||||||
|
path = urlParts[1];
|
||||||
|
const url = urlParts[2];
|
||||||
|
if (!groupId) {
|
||||||
|
const err = 'no group id defined';
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._xhr({
|
||||||
|
url,
|
||||||
|
responseType: 'json',
|
||||||
|
success: (response) => {
|
||||||
|
const rev = response.eTag;
|
||||||
|
if (!rev) {
|
||||||
|
this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts));
|
||||||
|
return callback && callback('no eTag');
|
||||||
|
}
|
||||||
|
this.logger.debug('Stated', path, rev, this.logger.ts(ts));
|
||||||
|
return callback && callback(null, { rev });
|
||||||
|
},
|
||||||
|
error: (err, xhr) => {
|
||||||
|
if (xhr.status === 404) {
|
||||||
|
this.logger.debug('Stated not found', path, this.logger.ts(ts));
|
||||||
|
return callback && callback({ notFound: true });
|
||||||
|
}
|
||||||
|
this.logger.error('Stat error', path, err, this.logger.ts(ts));
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
save(path, opts, data, callback, rev) {
|
||||||
|
this._oauthAuthorize((err) => {
|
||||||
|
if (err) {
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
this.logger.debug('Save', path, rev);
|
||||||
|
const ts = this.logger.ts();
|
||||||
|
|
||||||
|
const urlParts = this.genUrl(path);
|
||||||
|
const groupId = urlParts[0];
|
||||||
|
path = urlParts[1];
|
||||||
|
const url = urlParts[2] + ':/content';
|
||||||
|
if (!groupId) {
|
||||||
|
const err = 'no group id defined';
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._xhr({
|
||||||
|
url,
|
||||||
|
method: 'PUT',
|
||||||
|
responseType: 'json',
|
||||||
|
headers: rev ? { 'If-Match': rev } : null,
|
||||||
|
data,
|
||||||
|
statuses: [200, 201, 412],
|
||||||
|
success: (response, xhr) => {
|
||||||
|
rev = response.eTag;
|
||||||
|
if (!rev) {
|
||||||
|
this.logger.error('Save error', path, 'no eTag', this.logger.ts(ts));
|
||||||
|
return callback && callback('no eTag');
|
||||||
|
}
|
||||||
|
if (xhr.status === 412) {
|
||||||
|
this.logger.debug('Save conflict', path, rev, this.logger.ts(ts));
|
||||||
|
return callback && callback({ revConflict: true }, { rev });
|
||||||
|
}
|
||||||
|
this.logger.debug('Saved', path, rev, this.logger.ts(ts));
|
||||||
|
return callback && callback(null, { rev });
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.logger.error('Save error', path, err, this.logger.ts(ts));
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
list(dir, callback) {
|
||||||
|
this._oauthAuthorize((err) => {
|
||||||
|
if (err) {
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
this.logger.debug('List', dir);
|
||||||
|
const ts = this.logger.ts();
|
||||||
|
|
||||||
|
// console.warn('dir ', dir);
|
||||||
|
const urlParts = this.genUrl(dir);
|
||||||
|
const groupId = urlParts[0];
|
||||||
|
dir = urlParts[1];
|
||||||
|
const urlPath = groupId ? (dir ? ':/children' : '/drive/root/children') : '';
|
||||||
|
const url = urlParts[2] + urlPath;
|
||||||
|
// console.warn('urlParts', urlParts);
|
||||||
|
// console.warn('groupId ', groupId);
|
||||||
|
// console.warn('dir ', dir);
|
||||||
|
// console.warn('urlPath ', urlPath);
|
||||||
|
// console.warn('url ', url);
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
self._groupId = groupId;
|
||||||
|
|
||||||
|
this._xhr({
|
||||||
|
url,
|
||||||
|
responseType: 'json',
|
||||||
|
success: (response) => {
|
||||||
|
if (!response || !response.value) {
|
||||||
|
this.logger.error('List error', this.logger.ts(ts), response);
|
||||||
|
return callback && callback('list error');
|
||||||
|
}
|
||||||
|
this.logger.debug('Listed', this.logger.ts(ts));
|
||||||
|
let fileList;
|
||||||
|
if (!self._groupId) {
|
||||||
|
fileList = response.value
|
||||||
|
.filter((f) => f.displayName)
|
||||||
|
.map((f) => ({
|
||||||
|
name: f.displayName,
|
||||||
|
path: '/' + f.id,
|
||||||
|
rev: f.id,
|
||||||
|
dir: true
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
fileList = response.value
|
||||||
|
.filter((f) => f.name)
|
||||||
|
.map((f) => ({
|
||||||
|
name: f.name,
|
||||||
|
path: `/${self._groupId}${f.parentReference.path}/${f.name}`,
|
||||||
|
rev: f.eTag,
|
||||||
|
dir: !!f.folder
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return callback && callback(null, fileList);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.logger.error('List error', this.logger.ts(ts), err);
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(path, callback) {
|
||||||
|
this.logger.debug('Remove', path);
|
||||||
|
const ts = this.logger.ts();
|
||||||
|
|
||||||
|
const urlParts = this.genUrl(path);
|
||||||
|
const groupId = urlParts[0];
|
||||||
|
path = urlParts[1];
|
||||||
|
const url = urlParts[2];
|
||||||
|
if (!groupId) {
|
||||||
|
const err = 'no group id defined';
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._xhr({
|
||||||
|
url,
|
||||||
|
method: 'DELETE',
|
||||||
|
responseType: 'json',
|
||||||
|
statuses: [200, 204],
|
||||||
|
success: () => {
|
||||||
|
this.logger.debug('Removed', path, this.logger.ts(ts));
|
||||||
|
return callback && callback();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.logger.error('Remove error', path, err, this.logger.ts(ts));
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir(path, callback) {
|
||||||
|
this._oauthAuthorize((err) => {
|
||||||
|
if (err) {
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
this.logger.debug('Make dir', path);
|
||||||
|
const ts = this.logger.ts();
|
||||||
|
|
||||||
|
const urlParts = this.genUrl(path);
|
||||||
|
const groupId = urlParts[0];
|
||||||
|
path = urlParts[1];
|
||||||
|
const url = urlParts[2] + '/drive/root/children';
|
||||||
|
if (!groupId) {
|
||||||
|
const err = 'no group id defined';
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.stringify({ name: path.replace('/drive/root:/', ''), folder: {} });
|
||||||
|
this._xhr({
|
||||||
|
url,
|
||||||
|
method: 'POST',
|
||||||
|
responseType: 'json',
|
||||||
|
statuses: [200, 204],
|
||||||
|
data,
|
||||||
|
dataType: 'application/json',
|
||||||
|
success: () => {
|
||||||
|
this.logger.debug('Made dir', path, this.logger.ts(ts));
|
||||||
|
return callback && callback();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.logger.error('Make dir error', path, err, this.logger.ts(ts));
|
||||||
|
return callback && callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(enabled) {
|
||||||
|
this._oauthRevokeToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
_getOAuthConfig() {
|
||||||
|
let clientId = this.appSettings.teamsClientId;
|
||||||
|
let clientSecret = this.appSettings.teamsClientSecret;
|
||||||
|
let tenant = this.appSettings.teamsTenantId;
|
||||||
|
|
||||||
|
if (!clientId) {
|
||||||
|
if (Features.isDesktop) {
|
||||||
|
({ id: clientId, secret: clientSecret, tenantId: tenant } = TeamsApps.Desktop);
|
||||||
|
} else if (Features.isLocal) {
|
||||||
|
({ id: clientId, secret: clientSecret, tenantId: tenant } = TeamsApps.Local);
|
||||||
|
} else {
|
||||||
|
({ id: clientId, secret: clientSecret, tenantId: tenant } = TeamsApps.Production);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tenant = tenant || 'common';
|
||||||
|
|
||||||
|
let scope = 'Sites.ReadWrite.All Team.ReadBasic.All';
|
||||||
|
if (!this.appSettings.shortLivedStorageToken) {
|
||||||
|
scope += ' offline_access';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
||||||
|
tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||||
|
scope,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
pkce: true,
|
||||||
|
width: 600,
|
||||||
|
height: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StorageTeams };
|
|
@ -5,6 +5,7 @@ import { StorageFile } from 'storage/impl/storage-file';
|
||||||
import { StorageFileCache } from 'storage/impl/storage-file-cache';
|
import { StorageFileCache } from 'storage/impl/storage-file-cache';
|
||||||
import { StorageGDrive } from 'storage/impl/storage-gdrive';
|
import { StorageGDrive } from 'storage/impl/storage-gdrive';
|
||||||
import { StorageOneDrive } from 'storage/impl/storage-onedrive';
|
import { StorageOneDrive } from 'storage/impl/storage-onedrive';
|
||||||
|
import { StorageTeams } from 'storage/impl/storage-teams';
|
||||||
import { StorageWebDav } from 'storage/impl/storage-webdav';
|
import { StorageWebDav } from 'storage/impl/storage-webdav';
|
||||||
import { createOAuthSession } from 'storage/pkce';
|
import { createOAuthSession } from 'storage/pkce';
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ const ThirdPartyStorage = {
|
||||||
dropbox: new StorageDropbox(),
|
dropbox: new StorageDropbox(),
|
||||||
gdrive: new StorageGDrive(),
|
gdrive: new StorageGDrive(),
|
||||||
onedrive: new StorageOneDrive(),
|
onedrive: new StorageOneDrive(),
|
||||||
|
teams: new StorageTeams(),
|
||||||
webdav: new StorageWebDav()
|
webdav: new StorageWebDav()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ $fa-var-file-image: next-fa-glyph();
|
||||||
$fa-var-file-video: next-fa-glyph();
|
$fa-var-file-video: next-fa-glyph();
|
||||||
$fa-var-file-audio: next-fa-glyph();
|
$fa-var-file-audio: next-fa-glyph();
|
||||||
$fa-var-onedrive: next-fa-glyph();
|
$fa-var-onedrive: next-fa-glyph();
|
||||||
|
$fa-var-user-friends: next-fa-glyph();
|
||||||
$fa-var-question: next-fa-glyph();
|
$fa-var-question: next-fa-glyph();
|
||||||
$fa-var-sign-out-alt: next-fa-glyph();
|
$fa-var-sign-out-alt: next-fa-glyph();
|
||||||
$fa-var-sync-alt: next-fa-glyph();
|
$fa-var-sync-alt: next-fa-glyph();
|
||||||
|
|
Loading…
Reference in New Issue